URL請求處理流程:
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 示例:
- 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你好";
}
}
- Controller也可以指定路徑
下面hello方法對應的url變為:/hello/index
@RestController
@RequestMapping("/hello")
public class HelloController {
@RequestMapping("index")
public String hello() {
return "hello你好";
}
}
- 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
- 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,處理方法為:
get(int id, String name),則參數會一一對應;get(int id),則只會填充id的值;get(int id, String name, String email),則email的值為null;get(int id, int age, String name),則會出錯,因為age沒有值,會設置為null,但是age又是一個基本類型,無法設置為null。可以將age設置為Integer,則不過拋錯;get(User user),則會調用User的setter方法,id和name會被設置,user的其他屬性為null;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 集合/類參數
- 使用多個同名參數:適用於List, Array, Set;
- 使用[](中括號):適用於List, Array(請求體中),Map;
- 使用.(點號):對象;
- 逗號分隔:適用於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 方法保存參數值。
示例:
頁面代碼:
// 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,也可以通過正則表達式和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>
分析:
- 上傳文件時,form的enctype屬性必須是:"multipart/form-data";
- 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";
}
}
