Spring MVC 學習


URL請求處理流程:

graph LR id1(URL處理方法映射)-->id2(參數解析) id2-->id3(執行方法) id3-->id4(響應請求)

1. URL處理方法映射

1.1 Controller

頁面控制器,內部可以包含處理具體請求的方法(@RequestMapping)。

  • @Controller:返回視圖,如jsp或者html文件;
  • @RestController:返回JSON,如字符串,或者數組;

1.2 @RequestMapping

  • 作用:指定url請求的處理方法;
  • 作用范圍:
    • 用在Controller類上:指定上級路徑;
    • 用在Controller內部的方法上:url請求具體的處理方法;
  • 常用屬性:
    • value:url路徑,可以多個;
    • method:http方法,一般去get或者post;
  • 其他屬性:
    • params:對url參數做限制,如必須(不)包含某個參數,以及參數取值限制等;
    • headers:讀請求頭做限制;
    • consumes:能處理的類型(media type);
    • produces:返回的類型;

1.3 示例:

  1. url匹配

hello()方法可以匹配的路徑有:"/", "/hello", "/index";即在瀏覽器地址欄輸入:
http://localhost:8080/
http://localhost:8080/hello
http://localhost:8080/index
都會進入到hello方法中處理。

@RestController
public class HelloController {

    @RequestMapping(value = {"/", "/hello", "/index"})
    public String hello() {
        return "hello你好";
    }
}
  1. Controller也可以指定路徑

下面hello方法對應的url變為:/hello/index

@RestController
@RequestMapping("/hello")
public class HelloController {

    @RequestMapping("index")
    public String hello() {
        return "hello你好";
    }
}
  1. method示例

當不指定method屬性時,方法可以同時處理get和post方法。

@RequestMapping(value = "/hello", method = RequestMethod.GET)
指定只能處理get方法,等於:@GetMapping("/hello")

@RequestMapping(value = "/hello", method = RequestMethod.POST)
指定只能處理post方法,等於:@PostMapping("/hello")

@RestController
public class HelloController {

    @RequestMapping(value = "/hello", method = RequestMethod.POST)
    public String hello() {
        return "hello你好";
    }

}

上述寫法,在瀏覽器中訪問localhost:8080/hello會提示出錯,因為代碼中指定的時post方法,而瀏覽器直接訪問時,使用的時get方法。

HTTP Status 405 – 方法不允許
Type Status Report

消息 Request method 'GET' not supported

描述 請求行中接收的方法由源服務器知道,但目標資源不支持

Apache Tomcat/9.0.30
  1. produces可以解決返回亂碼

返回值在瀏覽器中出現亂碼,一般是由於沒有對返回內容指定編碼,導致SpringMVC使用了默認的ISO編碼。如下示例指定了使用utf-8編碼。

@RequestMapping(value = "/hello", produces = "text/html;charset=utf-8")
public String hello() {
    return "hello你好";
}

不推薦使用produces解決亂碼,因為使用轉換器更方便。

2. 參數解析

2.1 基本原理

將請求參數解析成key=value對,然后根據方法中參數類型和名稱,進行參數轉換和填充。

例如:url請求為http://localhost/user/get?id=1&name=Jim,處理方法為:

  1. get(int id, String name),則參數會一一對應;
  2. get(int id),則只會填充id的值;
  3. get(int id, String name, String email),則email的值為null;
  4. get(int id, int age, String name),則會出錯,因為age沒有值,會設置為null,但是age又是一個基本類型,無法設置為null。可以將age設置為Integer,則不過拋錯;
  5. get(User user),則會調用User的setter方法,id和name會被設置,user的其他屬性為null;
  6. get(User user, int id),則user.id和id都會被賦值;

2.2 參數來源:請求頭和請求體

get和post方法都可以在請求頭(request head,即在url后面的參數)中設置參數,但是只有post方法有請求體(request body,表單提交的數據就在這里);

HTTP/1.1 並未規定get不能有請求體,但一般Web服務器不會處理get方法的請求體。

2.3 參數編碼

2.3.1 請求頭參數編碼

的編碼一般不需要關注,瀏覽器默認是utf-8,tomcat8,9 對url的編碼也是utf-8。

@RequestMapping(value = "/hello")
public String hello(String msg) {
    System.out.println("msg: " + msg);
    return "hello你好";
}

當請求:GET http://localhost:8080/hello?msg=123中國時,idea中打印無亂碼。

msg: 123中國
2.3.2 請求體參數編碼

請求體長什么樣?

POST http://localhost:8080/rest-user/add
Content-Type: application/x-www-form-urlencoded

id=1&name=Jim吉姆

上面的id=1&name=Jim就是請求體,也是key=value格式的。注意get方法的請求體會被忽略的。

上述請求,在服務器端會出現亂碼。原因是方法體未設置編碼時,會被默認設置為iso編碼。

// Default character encoding to use when {@code request.getCharacterEncoding} 
// returns {@code null}, according to the Servlet spec.
org.springframework.web.util.WebUtils.DEFAULT_CHARACTER_ENCODING = "ISO-8859-1";

請求體可以通過Content-Type設置編碼。

POST http://localhost:8080/rest-user/add
Content-Type: application/x-www-form-urlencoded;charset=utf-8

id=1&name=Jim吉姆

上述設置編碼后,服務器端不會產生亂碼。

2.3.3 通過過濾器或攔截器編碼

如果不想在發送請求時設置編碼格式,還可以通過過濾器或者攔截器設置編碼。

  • 方法一:過濾器
    在web.xml的開頭添加如下配置:
<filter>
    <filter-name>encodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>utf-8</param-value>
    </init-param>
    <init-param>
        <param-name>forceRequestEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>encodingFilter</filter-name>
    <servlet-name>dispatcher</servlet-name>
</filter-mapping>
  • 方法二:攔截器

定義攔截器:

public class HelloInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        request.setCharacterEncoding("utf-8");
        return true;
    }
}

配置攔截器:

<mvc:interceptors>
    <mvc:interceptor>
        <bean class="com.bailiban.mvc.interceptor.HelloInterceptor" />
    </mvc:interceptor>
</mvc:interceptors>

攔截器使用了HttpServletRequest,需要導入依賴:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
    <scope>provided</scope>
</dependency>

2.4 參數轉換

瀏覽器中傳遞的傳輸是字符串,但我們方法中參數的類型卻多種多樣,它們是如何進行轉換的呢?

2.4.1 常見情景

常見的轉換SpringMVC已經幫我們處理了。
詳見:org.springframework.core.convert.converter.Converter接口的實現類。例如:StringToNumber類實現了字符串到數字類型(int,short,float等)的轉換。

2.4.2 String -> 自定義類

如2.1示例5所述,根據參數名和setter方法來轉換。這也是SpringMVC處理的,不需要我們手動處理。

2.4.3 自定義轉換方法

當我們需要將參數轉換為Date類型時,SpringMVC無法處理,需要我們自定義轉換方法。

public class StringToDateConverter implements Converter<String, Date> {

    private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    @Override
    public Date convert(String source) {
        try {
            return format.parse(source);
        } catch (ParseException e) {
            e.printStackTrace();
            return null;
        }
    }
}

配置轉換器:

<bean id="myConversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="com.bailiban.mvc.converter.StringToDateConverter" />
        </set>
    </property>
</bean>

將轉換器添加到annotation-drivern中

<mvc:annotation-driven conversion-service="myConversionService">
        <mvc:message-converters>
            <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                <property name="defaultCharset" value="utf-8" />
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>

2.5 參數相關注解

  • @RequestParam:用在方法的參數上,可以省略。
  • @RequestBody:獲取請求體,用在post方法中,也可以省略,SpringMVC會自動幫我們設置對象屬性;
  • @PathVaribale:位置參數,restfull風格url使用。
  • @ModelAttribute:當請求參數不完整時,可以使用它注解的方法。注意,同一Controller下的所有方法在運行前,都會執行其注解的方法,有時候可能並不是你想要的。
2.5.1 @ModelAttribute 使用示例
  • 前端請求:
POST http://localhost:8080/rest-user/add
Accept: text/html;charset=utf-8
Content-Type: application/x-www-form-urlencoded;charset=utf-8

id=1&name=Jim123中國
  • 后端處理:
@PostMapping("add")
public String add(User user) {
    System.out.println(user);
    userList.add(user);
    return user.toString();
}
  • @ModelAttribute 方法:
@ModelAttribute
public User getUser(int id) {
    User user = userList.stream().filter(u -> u.getId().equals(id)).findAny().orElse(null);
    user.setFriends(Arrays.asList("Lily", "Lucy"))
        .setDate(new Date());
    return user;
}
  • 請求結果:
User(id=1, name=Jim123中國, friends=[Lily, Lucy], date=Sat Jan 04 21:00:27 CST 2020)

我們可以看到,user的其他屬性也被賦值。而name屬性使用的是請求參數設置的值。注意@ModelAttribute 方法在請求映射的方法之前運行。

2.6 前端請求參數設置

2.6.1 基本類型

普通參數只需要前后端參數名稱一致即可。

2.6.2 自定義類

如2.1和2.4.2所述,與普通參數無異,只是SpringMVC使用了類的setter方法而已。

2.6.3 集合/類參數
  1. 使用多個同名參數:適用於List, Array, Set;
  2. 使用[](中括號):適用於List, Array(請求體中),Map;
  3. 使用.(點號):對象;
  4. 逗號分隔:適用於List, Array, Set;

示例:

1)包含對象屬性

@Data
public class User {

    private Integer id;
    private String name;
    private Set<String> friends;
    private Date date;
    private Account account;
    private Account[] accountList;
}

請求:

POST http://localhost:8080/rest-user/add
Accept: text/html;charset=utf-8
Content-Type: application/x-www-form-urlencoded;charset=utf-8

id=5&name=Tim&friends=lily,kate&account.money=100&accountList[0].money=1&accountList[1].money=2

結果:

User(id=5, name=Tim, friends=[lily, kate], date=null, account=Account(money=100.0), accountList=[Account(money=1.0), Account(money=2.0)])

2)逗號分割

GET http://localhost:8080/hello?msg=1,2,3
Accept: text/html
@RequestMapping(value = "/hello")
public String hello(String[] msg) {
    return String.join("###" , msg);
}

結果:

1###2###3

3)數組下標

POST http://localhost:8080/rest-user/add
Accept: text/html;charset=utf-8
Content-Type: application/x-www-form-urlencoded;charset=utf-8

id=5&name=Tim&friends[0]=lily&friends[1]=kate

3. 方法執行

通過映射找到url處理方法,該方法除了業務邏輯外,還需要考慮以何種方式響應。

4. 響應請求

4.1 返回json格式

簡單的字符串可以直接返回,但很多時候,我們直接返回的是對象。此時需要工具幫我們將類轉換為json字符串。

只需要加入如下依賴即可,SpringMVC會幫我們接收json格式參數(轉換對象),並將對象轉換為json返回,甚至會幫我們把字符編碼設置為utf-8。

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>${jackson-version}</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>${jackson-version}</version>
</dependency>

相關源碼(AnnotationDrivenBeanDefinitionParser.java):

	jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
					ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
---
    if (jackson2Present) {
		beanDef.getPropertyValues().add("requestBodyAdvice",
				new RootBeanDefinition(JsonViewRequestBodyAdvice.class));
	}
---
	if (jackson2Present) {
		beanDef.getPropertyValues().add("responseBodyAdvice",
				new RootBeanDefinition(JsonViewResponseBodyAdvice.class));
	}
---
    if (jackson2Present || gsonPresent) {
		defaultMediaTypes.put("json", MediaType.APPLICATION_JSON_VALUE);
	}
---
	if (jackson2Present) {
		Class<?> type = MappingJackson2HttpMessageConverter.class;
		RootBeanDefinition jacksonConverterDef = createConverterDefinition(type, source);
		GenericBeanDefinition jacksonFactoryDef = createObjectMapperFactoryDefinition(source);
		jacksonConverterDef.getConstructorArgumentValues().addIndexedArgumentValue(0, jacksonFactoryDef);
		messageConverters.add(jacksonConverterDef);
	}

為了使我們的映射方法能夠返回 json,需要:

  • 在類上添加@RestController注解;
  • 或者類上為@Controller注解,然后在方法上添加@ResponseBody注解;

4.2 響應頁面

直接返回頁面名即可。但一般我們使用了模板引擎時,會向模板文件傳遞數據,如何傳遞呢?

為了能夠正確找到文件,一般需要如下配置:

   <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" >
       <property name="prefix" value="/WEB-INF/pages/" />
       <property name="suffix" value=".jsp" />
   </bean>

在映射方法中,添加一個ModelMap類型的數據,就可以幫我們向模板文件傳遞數據了。

示例代碼:

@RequestMapping(value = "get")
public String get(int id, ModelMap model) {
    model.addAttribute("user",
            userList.stream().filter(u -> u.getId().equals(id)).findAny().orElse(null));
    return "user";
}

該代碼做了什么:

  • 添加參數:ModelMap model;
  • 向model添加屬性:model.addAttribute("user", xxx);
  • 返回user對應於web\WEB-INF\pages\user.jsp文件;

user.jsp 中,通過${user.id}${user.name}獲取user的id和name值:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page import="java.util.Date" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<html>
<head>
    <title>Title</title>
    <meta http-equiv="content-type" content="text/html;charset=utf-8">
</head>
<body>

<c:if test="${!empty user}">
    <form action="${pageContext.request.contextPath}/user/update" method="post">
        <div><label>ID:<input name="id" value="${user.id}"></label></div>
        <div><label>Name:<input name="name" value="${user.name}"></label></div>
        <div><input type="submit" value="submit"></div>
    </form>
</c:if>

</body>
</html>

注意,方法中的參數也會默認傳給頁面。下面方法中,參數user會到頁面中。

@RequestMapping(value = "get2")
public String get(User user) {
    int id = user.getId();
    User user1 = userList.stream().filter(u -> u.getId().equals(id)).findAny().orElse(null);
    user.setName(user1.getName());
    return "user";
}

為了正常使用jstl/core標簽,需要添加依賴:

<dependency>
    <groupId>taglibs</groupId>
    <artifactId>standard</artifactId>
    <version>1.1.2</version>
</dependency>
<dependency>
    <groupId>jstl</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>

4.3 重定向

核心:解決參數丟失

重定向會使瀏覽器再發送一次(不同url)請求,第一次提交的參數會丟失。
解決辦法:

使用 RedirectAttributes 參數,並使用 addFlashAttribute 方法保存參數值。

示例:

graph LR id1(login)-- 重定向 -->id2(home)

頁面代碼:

// login.jsp
    <form action="${pageContext.request.contextPath}/user/login" method="post">
        <label>ID: <input name="id" value=""></label><br>
        <label>Name: <input name="name" value=""></label><br>
        <input type="submit" value="Login">
    </form>
// user.jsp
    <div id="user">
        <label>ID: ${user.id}</label><br>
        <label>Name: ${user.name}</label><br>
    </div>

Controller代碼:

    @RequestMapping("login")
    public String login(User user, RedirectAttributes redirectAttributes) {
        System.out.println(user);
        if (user.getId() != null) {
            if (user.getId() != null &&
                (userList.stream().anyMatch(u -> u.getId().equals(user.getId()) &&
                        u.getName().equals(user.getName())))) {
                redirectAttributes.addFlashAttribute(user);
                return "redirect:/user/home";
            }
        }
        return "login";
    }

    @RequestMapping("home")
    public String home(User user) {
        if (user.getId() == null) {
            return "redirect:/user/login";
        }
        return "home";
    }

重點:

  • login(User user, RedirectAttributes redirectAttributes):方法添加RedirectAttributes參數;
  • redirectAttributes.addFlashAttribute(user);:傳遞參數;
  • return "redirect:/user/home";:重定向到home頁面;
  • home(User user):home方法會從redirectAttributes中獲取user對象;

注意:完成重定向后,user值會清空。可自行刷新home頁面,看看會發生什么!

如果想一直保存user的值,該如何處理呢?后續會在登錄模塊介紹。

4.4 forward

forward相對簡單,因為參數沒有丟失。

    @RequestMapping("login1")
    public String login(User user) {
        return "forward:/user/login";
    }

當訪問 /user/login1時,會丟給/user/login處理,user參數也會自動傳遞。

效果跟下面寫法是一樣的:

    @RequestMapping({"login", "login1"})
    public String login(User user, RedirectAttributes redirectAttributes) {
    // ...
    }

說明:@RequestMapping可以指定多個url映射到相同的方法。

5. 登錄處理

通過對用戶登錄的學習,我們可以進一步看到SpringMVC提供了哪些其他有用的功能。

5.1 @SessionAttributes

4.3重定向的學習中,我們使用了RedirectAttributes參數保存user信息,但是在刷新home頁面后,user信息丟失了。

@SessionAttributes 可以保存在整個Session使用的數據,很適合保存登錄信息。

使用示例:

  • Controller上使用@SessionAttributes,並通過類型(User.class)指定Session保存的的是用戶信息。
@Controller
@RequestMapping("/user")
@SessionAttributes(types = {User.class})
public class UserController {
}
  • login處理中,將user信息保存的model中model.addAttribute(user);
    @RequestMapping({"login", "login2"})
//    public String login(User user, RedirectAttributes redirectAttributes) {
    public String login2(User user, ModelMap model) {
        System.out.println(user);
        if (user.getId() != null) {
            if (user.getId() != null &&
                (userList.stream().anyMatch(u -> u.getId().equals(user.getId()) &&
                        u.getName().equals(user.getName())))) {
//                redirectAttributes.addFlashAttribute(user);
                model.addAttribute(user);
                return "redirect:/user/home";
            }
        }
        return "login";
    }

此時,在home頁面就可以正常訪問user用戶信息,且不會出現刷新丟失的問題。

  • logout中,可以清除登錄信息:sessionStatus.setComplete();
    @RequestMapping("logout")
    public String logout(SessionStatus sessionStatus) {
        // 清除session,即清除user對象0                                 
        sessionStatus.setComplete();
        return "redirect:/user/login";
    }

5.2 登錄驗證

在之前的處理中,我們把登錄驗證放在方法里面,如home方法通過條件:if (user.getId() == null)來判斷是否登錄:

    @RequestMapping("home")
    public String home(User user) {
        if (user.getId() == null) {
            return "redirect:/user/login";
        }
        return "home";
    }

但如果所有頁面都需要登錄只會才能訪問,在每個映射方法都添加登錄驗證是不可取的。可以使用攔截器解決。

創建LoginInterceptor攔截器

public class LoginInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 不攔截 login
        if (request.getRequestURI().contains("/login")) {
            return true;
        }
        HttpSession session = request.getSession();
        User user = (User) session.getAttribute("user");
        if (user == null || user.getId() == null) {
            // 轉發
//            request.getRequestDispatcher("/user/login").forward(request, response);
            // 重定向
            response.sendRedirect("/user/login");
            return false;
        }
        return true;
    }
}

login中設置Session級別的user信息:

login方法中添加HttpSession參數,調用其session.setAttribute("session_user", user);方法添加user參數;
示例:

    @RequestMapping("login")
    public String login2(User user, HttpSession session) {
        if (session.getAttribute("user") != null)
            return "redirect:/user/home";

        if (user.getId() != null &&
                (userList.stream().anyMatch(u -> u.getId().equals(user.getId()) &&
                        u.getName().equals(user.getName())))) {
                session.setAttribute("user", user);
                return "redirect:/user/home";
        }
        return "login";
    }

home頁面使用user信息,使用@SessionAttribute("user")

    @RequestMapping("home")
    public String home(@SessionAttribute("user") User user) {
        return "home";
    }

更新user

    @PostMapping("update")
    public String update(User user, HttpSession session) {
        System.out.println(user);
        for (int i=0; i<userList.size(); i++) {
            if (userList.get(i).getId().equals(user.getId())) {
                userList.set(i, user);
            }
        }
        session.setAttribute("user", user);
        return "redirect:/user/home";
    }

6. 參數校驗

6.1 使用Annotaion JSR-303標准的驗證

導入Hibernate Validator依賴:

    <dependency>
        <groupId>org.hibernate.validator</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>6.1.0.Final</version>
    </dependency>

可使用如下注解對屬性進行限制:

限制 說明
@Null 限制只能為null
@NotNull 限制必須不為null
@AssertFalse 限制必須為false
@AssertTrue 限制必須為true
@DecimalMax(value) 限制必須為一個不大於指定值的數字
@DecimalMin(value) 限制必須為一個不小於指定值的數字
@Digits(integer,fraction) 限制必須為一個小數,且整數部分的位數不能超過integer,小數部分的位數不能超過fraction
@Future 限制必須是一個將來的日期
@Max(value) 限制必須為一個不大於指定值的數字
@Min(value) 限制必須為一個不小於指定值的數字
@Past 限制必須是一個過去的日期
@Pattern(value) 限制必須符合指定的正則表達式
@Size(max,min) 限制字符長度必須在min到max之間
@Past 驗證注解的元素值(日期類型)比當前時間早
@NotEmpty 驗證注解的元素值不為null且不為空(字符串長度不為0、集合大小不為0)
@NotBlank 驗證注解的元素值不為空(不為null、去除首位空格后長度為0),不同於@NotEmpty,@NotBlank只應用於字符串且在比較時會去除字符串的空格
@Email 驗證注解的元素值是Email,也可以通過正則表達式和flag指定自定義的email格式

示例代碼:

  • User屬性添加限制@NotEmpty(message="用戶名不能為空!")
public class User {

    private Integer id;
    @NotEmpty(message="用戶名不能為空!")
    private String name;
}    
  • 參數校驗@Validated User user
    @PostMapping("update")
    public String update(@Validated User user, HttpSession session){
    }

當提交的用戶名為空時,會拋異常:

org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.logException Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'user' on field 'name': rejected value []; codes [NotEmpty.user.name,NotEmpty.name,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.name,name]; arguments []; default message [name]]; default message [用戶名不能為空!]]

關於嵌套校驗:屬性上添加@Valid注解即可。

6.2 自定義校驗器

public class UserValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        // 判斷是否為User類        
        return User.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        User user = (User) target;
        // 判斷name是否為空        
        if (StringUtils.isEmpty(user.getName())) {
            errors.rejectValue("name", "-1", "用戶名不能為空!");
        }
    }
}
  • 添加校驗器,在UserController中添加如下代碼:
    @InitBinder
    public void initBinder(DataBinder binder){
        binder.replaceValidators(new UserValidator());
    }

@InitBinder方法也可以添加到@ControllerAdvice注解的類中,或者設置到<annotation-driven>validator屬性中。

  • 使用時,user參數也需要添加@Validated注解

6.3 自定義注解校驗器

  • 定義校驗器:NameValidator
public class NameValidator implements ConstraintValidator<NameValidation, String> {
    @Override
    public void initialize(NameValidation constraintAnnotation) {

    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (StringUtils.isEmpty(value))
            return false;
        return true;
    }
}
  • 定義校驗器注解:NameValidation
@Constraint(validatedBy = NameValidator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
public @interface NameValidation {

    public String message() default "名稱不合法";
    public Class<?>[] groups() default {};
    public Class<? extends Payload>[] payload() default {};

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
    @interface List {
        NameValidation[] value();
    }
}
  • User.name上添加注解:@NameValidation
public class User {

    private Integer id;
    @NameValidation(message="自定義注解校驗器:用戶名不能為空!")
    private String name;
}    
  • 使用時,與前面兩種方法相同。user參數添加@Validated注解即可。

7. 異常處理

在參數校驗中,我們直接拋出了異常。這些異常也可以交給Spring MVC處理。

  • 定義一個攔截器獲取請求的方式:頁面 or rest.
public class ExceptionInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        boolean isRest = method.isAnnotationPresent(ResponseBody.class) ||
                handlerMethod.getBeanType().isAnnotationPresent(RestController.class);
        request.setAttribute("isRest", isRest);
        return true;
    }
}
  • 定義一個@ControllerAdvice注解的類,用於接收並處理異常;
@ControllerAdvice
public class MyExceptionHandler {

    @ExceptionHandler(BindException.class)
    public ModelAndView handlerException(HttpServletRequest request, HttpServletResponse response, Exception e) {
        ModelAndView modelAndView = null;
        List<String> errMsg = new ArrayList<>();
        String errStr = null;

        BindException be = (BindException) e;
        List<FieldError> errors = be.getBindingResult().getFieldErrors();
        for (FieldError error: errors) {
            errMsg.add(error.getField() + ": " + error.getDefaultMessage());
        }
        errStr = String.join(", ", errMsg);

        boolean isRest = (boolean) request.getAttribute("isRest");
        if (isRest) {
            modelAndView = new ModelAndView(new MappingJackson2JsonView());
            modelAndView.addObject("code", 500);
            modelAndView.addObject("message", errStr);
            return modelAndView;
        }
        Map<String, String> model = new HashMap<>();
        model.put("message", errStr);
        return new ModelAndView("/error/500", model);
    }
}
  • 處理異常的頁面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>500</title>
</head>
<body>
服務器異常。錯誤信息:${message}。
</body>
</html>

注:如果沒有自定義異常處理,Tomcat會返回錯誤碼為400頁面。我們也可以將在web.xml中指定400對應的頁面路徑。

<error-page>
    <error-code>400</error-code>
    <location>/WEB-INF/pages/error/400.jsp</location>
</error-page>

8. 靜態文件訪問

當我們配置DispatcherServlet處理所有url請求時,靜態文件也會被它處理。而靜態文件沒有對應的映射方法,直接訪問靜態頁面會404錯誤。
可以在dispatcher-servlet.xml中配置靜態文件的處理方式:

<mvc:resources mapping="/**" location="/WEB-INF/static/" />

假設靜態文件存放在"/WEB-INF/static/"路徑下,那么我們直接就能訪問靜態文件了:

<link rel="stylesheet" href="/css/main.css">

注:main.css位置在:"/WEB-INF/static/css/main.css"。

9. 文件上傳

  • 導入用於文件上傳的依賴:
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.4</version>
    </dependency>
  • 用於文件上傳的jsp文件
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Upload</title>
    <script type="text/javascript" src="/js/jquery-3.4.1.min.js"></script>
    <link rel="stylesheet" href="/css/main.css">
</head>
<body>
<form id="upload" class="content-center" enctype="multipart/form-data">
    <label>名稱:<input name="fileName"></label><br>
    <label>文件:<input type="file" name="uploadFile"></label><br>
    <input type="button" value="上傳" onclick="fileSubmit()">
</form>
<script>
    function fileSubmit() {
        const form = new FormData(document.getElementById("upload"));
        $.ajax({
            url: "/upload",
            data: form,
            type: "post",
            processData: false,
            contentType: false,
            success: (data) => {
                alert(data);
            }
        });
    }
</script>
</body>
</html>

分析:

  1. 上傳文件時,form的enctype屬性必須是:"multipart/form-data";
  2. ajax中:
    • 獲取form內容:const form = new FormData(document.getElementById("upload"));
    • processData需要設置為false;
    • contentType需要設置為false;
  • Java代碼
@Controller
public class FileUploadController {

    @GetMapping("/upload")
    public String upload() {
        return "upload";
    }

    @PostMapping("/upload")
    @ResponseBody
    public String upload(String fileName, MultipartFile uploadFile, HttpServletRequest request) {
        String fName = "";
        String originName = uploadFile.getOriginalFilename();
        String ext = originName.substring(originName.lastIndexOf(".") + 1);
        String uuid = UUID.randomUUID().toString();
        if (!StringUtils.isEmpty(fileName)) {
            fName = fileName + "_" + uuid + "." + ext;
        } else {
            fName = uuid + originName;
        }
        ServletContext context = request.getServletContext();
        String basePath = context.getRealPath("/upload");
        String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
        File f = new File(basePath + "/" + date);
        if (!f.exists()) {
            f.mkdirs();
        }
        try {
            uploadFile.transferTo(new File(f, fName));
        } catch (IOException e) {
            e.printStackTrace();
            return "fail";
        }
        return "success";
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM