概述
由于以前学习Spring的时候学习的不太系统,所以通过本篇及以下几篇文章来重新梳理下Spring中常用注解的使用,从而加深对Spring注解的理解,本篇文章来看下Spring-web中常用的一些注解。
- 本系列学习的Spring版本是:4.3.14,工程基于全注解的实现。
- 强烈建议学习的时候先看官网文档:https://docs.spring.io/spring/docs/4.3.14.RELEASE/spring-framework-reference/html/mvc.html
- 本篇注解包括:
RequestParam
,PathVariable
,RequestBody
,ResponseBody
,RestController
,InitBinder
,RequestMapping
,GetMapping
,ControllerAdvice
,RestControllerAdvice
,ExceptionHandler
,ModelAttribute
。
1. RequestParam注解
通常在Spring-MVC后台我们进行获取参数的时候,一种是通过request.getParameter()
的方式来获取方式,不过这种方式太古老了;另一种方式就是通过注解的方式将参数匹配到对应的对象上,而RequestParam注解则是其中的一种。我们来简单看下:
@GetMapping("/test.html")
@ResponseBody
public User test(@RequestParam(value = "user") String user) {
return user;
}
然后我们在请求的时候可以通过表单传值的形式传递参数:localhost:8080/test.html?user=test
;该注解一共有4个参数:
value
,前端传入的参数名称,通过该名称来绑定参数到相应的对象上;name
,和value参数功能一样,是Spring4.2之后添加的,目的么,应该是为了代替原先value这个属性不太直白的描述方式;required
,该参数是否必须,默认是true,也就是必须要传入,如果不传入参数的话,会直接提示异常400;defaultValue
,如果没有传递参数,参数的默认值。当然,如果required=true的话,那该属性就没有意义了。
该注解的一些注意事项:
- 当设置required=false,也就是可以不传参数的时候,会给参数赋值为null,这时候如果参数的类型是基本类型 int 等,则会直接报错,所以定义参数的时候能优先使用包装类型就使用包装类型;
- 一般情况下,基于约定,如果方法的参数中没有使用该注解,那么前端的参数将绑定到同名的查询参数上。因为@RequestParam参数并不是必须的,这种情况下不传递参数也不会提示异常,和required=false的情况类似。
- 该注解一般用于表单提交的方式,也就是用于
Content-Type
是application/x-www-form-urlencoded
编码的提交。
- 该注解一般用于表单提交的方式,也就是用于
这里再多说一句,在绑定参数的时候,如果要绑定的参数不是基础类型而是对象的话,那么RequestParam是不支持的,这时候可以直接不使用该注解,采用默认的匹配方式即可。
(@RequestParam(value = "user", required = false, defaultValue = "test") User user)
如果要绑定数组的话,针对GET请求可以:
public String test(@RequestParam("ids") List<String> ids) {
请求路径为:http://localhost:8080/world/test.html?ids=12,123
,使用逗号进行分割即可。
2. PathVariable注解
Spring MVC中用于将URL中的占位符{xxx}
,绑定到控制器方法的参数上,该注解特别适用于REST的请求方式:
@GetMapping("/test.html/{user}")
@ResponseBody
public User test(@PathVariable("user") String user) {
}
该注解有三个参数:
value
,URL中占位符的名称,通过该名称来绑定对应的参数;name
,同样,和value参数功能一样,是Spring4.3.3之后添加的参数,目的应该也是用于代替原先value属性不太直白的描述方式;required
,该参数表示是否必须,默认是true,也就是必须要传入,如果不传入参数的话,会直接提示异常。这个参数需要注意下,接下来我们来看下。
比如我们想我们的test方法同时匹配/test.html
和/test.html/{user}
这两个url,我们可能会直接按照如下的方式:
@GetMapping(value = "/test.html/{user}")
@ResponseBody
public String test(@PathVariable(name = "user", required = false) String user) {
System.out.println(user);
}
这种方式可以匹配/test.html/{user}
,但匹配不了/test.html
,提示404异常,因为匹配的时候根据默认的Ant方式的AntPathMatcher匹配器,这是两个url,无法直接匹配,那该如何实现呢?
不过,根据api对应的文档,我们能大致了解如何来解决这个问题:
Whether the path variable is required.
Defaults to true, leading to an exception being thrown if the path variable is missing in the incoming request. Switch this to false if you prefer a null or Java 8 java.util.Optional in this case. e.g. on a ModelAttribute method which serves for different requests.
我们可以通过Java8中的Optional 来将该参数设置为可选的,但是我们的RequestMapping里必须要列出所有可能匹配的url:
@GetMapping(value = {"/test.html/{user}", "/test.html"})
@ResponseBody
public String test(@PathVariable(name = "user") Optional<String> user) {
}
这种情况下,required=false并不是必须的,也就是说这时候该属性不生效,这个时候就可以解决我们上面的这个问题了。不过我们也可以不借助于java.util.Optional
来解决这个问题,这个时候就需要借助参数required=false
来实现了:
@GetMapping(value = {"/test.html/{user}", "/test.html"})
@ResponseBody
public String test(@PathVariable(name = "user", required = false) String user) {
}
其实和上面那种实现方式类似,也就是直接参数匹配,不借助于Optional类,但这时候required就必须是false了。这里参考:https://stackoverflow.com/questions/47567364/optional-pathvariable-in-rest-controller-spring-4
这里需要注意下,这个特性是Spring4.3.3之后支持的。
下面再看简单说下使用该注解需要注意的情况:
- PathVariable的参数可以是任何简单类型,比如int,long,Date等等,如果不是这些基础类型,Spring会抛出TypeMismatchException异常,不过我们也可以自定义支持类型;
- 由于是绑定URL中的参数,所以就需要考虑URL中一些特殊的字符的处理,比如空格``,点号
.
,问号?
,斜杠号/
等,当我们进行参数绑定的时候记得做相应的处理。- 由于URL毕竟是要暴露对外的,所以要注意相应的安全性,避免别人根据某一个参数可以推断出另外的值,从而可能获取到别人相关的数据,也就是需要注意下敏感数据与公共数据的问题,其实这也是REST请求中要注意的问题。
- 和上文说的类似,就是要考虑下使用场景问题。
3. RequestBody注解
该注解和RequestParam有点类似,同样用于绑定参数,但不是用于绑定表单提交的数据,也就是不是用于绑定Content-Type
是application/x-www-form-urlencoded
情况下的参数,一般用于绑定Content-Type
是application/json
,application/xml
等类型的参数。
我们可以通过配置HttpMessageConverter来支持我们的Content-Type
类型,一般情况下,我们使用最多的是application/json
这种格式的参数传递,而在Spring中,默认支持json传递对象的converter是MappingJackson2HttpMessageConverter
。
该注解只有一个参数:
required
,参数是否必需,默认是true,Spring3.2版本引入。
@GetMapping(value = {"/test.html"})
@ResponseBody
public String test(@RequestBody(required = false) User user) {
}
Spring其实还默认提供了多种HttpMessageConverter,比如StringHttpMessageConverter
(Content-Type
为text/plain
),MappingJackson2XmlHttpMessageConverter
(Content-Type
为application/xml
,text/xml
)等,如果想了解更多,可以支持查看HttpMessageConverter的各个实现类。
有两点可能需要注意下:
- 使用RequestBody的时候,注意参数名称什么的要匹配,如果传入了一个后端对象里没有的参数,会直接提示参数不匹配400异常;
- 由于HttpMessageConverter有一个
FormHttpMessageConverter
的实现,所以说RequestBody也是支持Content-Type
为application/x-www-form-urlencoded
的情况的,只不过处理的结果放到了一个MultiValueMap<String, ?>
中,不过这种情况并不常见,如果想了解更多的话,可以查看下FormHttpMessageConverter的文档,里面有详细的说明。
4. ResponseBody注解
该注解和RequestBody是对应的,RequestBody是用于请求的时候传递相应格式的数据,而该注解则是响应的时候返回相关格式的数据。关于ResponseBody就不多说了,具体的配置相关的东西和RequestBody是一致的,因为这两个注解是对应的关系。
5. RestController注解
Spring4.0之后引入的注解,是@Controller
注解和@ResponseBody
注解的结合。添加该注解后,表明这个类中的所有方法都将具有@ResponseBody
的特性,也就是所有的返回试图的方法都将直接返回字符串,而配置的试图解析器也不会生效。
其他的话,功能和Controller一样,参数也是只有value一个参数。
6. InitBinder注解
在Spring MVC中,我们经常会涉及到表单中的日期字符串和JavaBean对象中的Date类型的转换,而默认情况下,Spring MVC是不支持这种格式转换的,这时候就需要使用@InitBinder注解来自定义转换类型。
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
}
然后定义我们的控制器参数:
@GetMapping(value = {"/test.html"})
public String test(@RequestParam("date") Date date, @RequestParam("user") String user) {
System.out.println(date);
}
打印结果:
Sat Dec 09 00:00:00 CST 2017
使用@InitBinder注解,然后借助WebDataBinder类,这样在绑定表单前,先注册这些编辑器。其实Spring提供了许多类型转换的实现,比如CustomDateEditor、CustomBooleanEditor、CustomNumberEditor、CustomMapEditor等等,我们可以根据我们的需要有选择的注册这些编辑器。
一般情况下,我们将该@InitBinder放于使用ControllerAdvice所配置的全局对象中,这样,它将对所有的控制器方法都生效。
7. RequestMapping GetMapping等注解
7.1 RequestMapping
RequestMapping这个注解应该是我们使用注解最多的几个之一,这个注解会将 HTTP 请求映射到 MVC 和 REST 控制器对应的处理方法上,可用于控制器的类或者方法上。
我们来看下它的参数:
name
,方法映射的名称;value
,匹配的请求的url数组,也就是path的别名;path
,匹配的请求的url数组,和value功能相同,Spring4.2引入该参数;method
,HTTP请求的参数类型,数组格式可支持多种类型,GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE。如果用于类级别上,所有的方法都将支持此映射方法类型;params
,映射请求的参数,也是数组格式,支持myParam
,myParam=myValue
,flag!=true
格式,只有参数满足相应的条件时才会映射这个请求;举个简单的例子:
@GetMapping(value = {"/test.html"}, params = "flag=true")
public String test(@RequestParam("date") Date date, @RequestParam("flag") Boolean flag) {
}
当请求到这个方法,并且参数包含flag=true
时才会进入这个方法,执行相应的操作,否则就会提示400异常。也就是说请求的url必须包含该参数,而不管方法的参数中是否有该参数。
Expressions can be negated by using the "!=" operator, with such parameters having to be present in the request (allowed to have any value). Finally, "!myParam" style expressions indicate that the specified parameter is not supposed to be present in the request.
headers
,支持的请求头类型,数组格式,同样也是支持My-Header
,My-Header=myValue
,My-Header!=myValue
格式的类型,当然还支持通配符(*),比如:
@RequestMapping(value = "/something", headers = "content-type=text/*")
这种情况下将会匹配Content-Type 是text/html
,text/plain
等类型。同样如果需要了解更多,可自行查看具体的HTTP请求头类型。
consumes
,指定请求的提交内容类型(Content-Type),只有Content-Type匹配所给出的类型时,才进行请求映射操作。数组类型,支持通配符,并且支持 "!" 操作符,比如"!text/plain"
,它将匹配除了text/plain
之外的所有Content-Type类型。
consumes = "text/plain"
consumes = {"text/plain", "application/*"}
produces
,和consumes
相反,前者指的是提交时候的数据类型Content-Type,也就是客户端发送的数据类型,而produces
指的是客户端接收的数据类型Accept。它是和consumes
相对应的,比如说我们使用UTF-8生成了一个JSON数据,则返回的时候应该设置application/json; charset=UTF-8
。
produces = "text/plain"
produces = {"text/plain", "application/*"}
produces = "application/json; charset=UTF-8"
7.2 GetMapping
在Spring4.3之后,Spring引入了一些组合级的注解来简化常用HTTP方法的映射,这些注解分别是:GetMapping
,PostMapping
,PutMapping
,DeleteMapping
,PatchMapping
。比如GetMapping
注解就相当于@RequestMapping(method = RequestMethod.GET)
,它们的参数什么的都和RequestMapping一样,就不多说了,大家可以有选择的使用。
8. ControllerAdvice RestControllerAdvice注解
该注解用于为控制器做一些全局配置,可以通过配置扫描路径来指定为哪些控制器进行配置,默认情况下,@ControllerAdvice中的方法适用于所有控制器。
一般情况下,配置了ControllerAdvice注解的类一般会包含@ExceptionHandler注解,@InitBinder注解和@ModelAttribute注解,这些方法将适用于所有被扫描到的控制器的RequestMapping方法。
来看一下它的几个参数:
basePackages
,所要管理的控制器所在的包,数组格式。比如:
@ControllerAdvice(basePackages={"org.my.pkg", "org.my.other.pkg"})
@ControllerAdvice(basePackages="org.my.pkg")
value
,该属性是basePackages
属性的别名,目的是为了允许更简洁的声明。basePackageClasses
,指定特定的控制器类,该全局配置只对这些类生效,同样是数组格式。比如:@ControllerAdvice(basePackageClasses = HelloController.class)
,则该ControllerAdvice所修饰的配置类只对HelloController生效。assignableTypes
,和上面的basePackageClasses
类似,这个属性也是为了指定特定的控制器类,不同的是,该属性指定的是基类,而该控制器的配置对该基类的继承类也都生效。annotations
,指定所有使用了该注解类型的控制器类,数组类型。比如:@ControllerAdvice(annotations = Controller.class)
,也就是该配置对所有使用Controller注解的类生效。
另外,对于RestControllerAdvice注解,功能和ControllerAdvice一样,但需要注意一点:RestControllerAdvice所修饰的@ExceptionHandler将自动具有@ResponseBody的功能,也就是在ControllerAdvice下原先我们所返回的视图,在RestControllerAdvice下,将会返回字符串,而不会进入对应的视图页面。
@ExceptionHandler
public String exception(Exception e, Model model) {
model.addAttribute("message", e.getMessage());
return "/exception";
}
在RestControllerAdvice下,将直接返回"/exception",而不会进入对应的异常视图页面。
ControllerAdvice最常用的情况就是和ExceptionHandler注解一起,构建全局的异常管理页面。
9. ExceptionHandler注解
上文已经大致了解过该注解,用于在异常情况下的后续操作,可以跳转至异常页面,也可以指定特定的异常信息返回,最常用的情况上面已经说过,就是用于和ControllerAdvice注解配合使用。参数只有一个:
value
,用于指定具体的异常类型,默认情况下的异常为方法参数列表中列出的所有异常;使用时我们可以在方法参数中指明具体的异常类,也可以配置该参数指明异常类。
10. ModelAttribute注解
ModelAttrribute注解我们也经常用到,一般情况下我们是用于两方面:
- 用于方法上,这样的话,ModelAttrribute注解标记的方法将在所有RequestMapping所标记的方法之前执行,如果有返回值,则自动将该返回值加入到模型对象中,比如Model对象或ModelMap对象;
- 运用在参数上,又分为两种情况,参数是单个参数还是对象;
参数是单个参数:先去Model或ModelMap中查找是否有该对象,如果有,注入到该参数中(客户端传过来的值将不再绑定),如果没有,再将客户端传过来的值绑定到该参数上,并且自动将该参数加入到Model或ModelMap中,以便View层使用;
参数是对象:同样,先去Model或ModelMap中查找是否有该对象,如果有,注入到该参数中,然后将客户端传过来的值绑定到参数上,这时候会覆盖掉数据模型中的该对象,也就是说URI对象具有高优先级;
- 运用在参数上,又分为两种情况,参数是单个参数还是对象;
我们先来看下该注解的几个参数,然后再仔细来看下上面这两种情况:
name
,要绑定到Model的对象的名称,默认情况下,是根据方法参数类型或方法返回类型推断出来的,比如mypackage.OrderAddress
的名称是orderAddress
,List<mypackage.OrderAddress>
的名称是orderAddressList
(该属性Spring4.3之后才引入);value
,name的别名,和name功能完全一致;binding
,是否允许在@ModelAttribute方法参数或@ModelAttribute方法返回值上声明数据绑定,默认是true。
接下来我们来详细了解下这两种情况,以下的几种情况都使用ModelMap来测试:
(1)用于方法上,方法返回值为void的情况:
@ModelAttribute
public void init(ModelMap modelMap) {
modelMap.addAttribute("userId", "123");
}
对应的测试接口:
@RequestMapping("/hello.html")
public String hello(ModelMap modelMap, String id) {
modelMap.addAttribute("id", "456");
System.out.println(modelMap);
return "hello";
}
可以看到,在执行hello方法之前,init方法已经执行,所以ModelMap中已经有了"userId"对象。
(2)用于方法上,方法返回值为对象的情况,这时候ModelMap参数可以不传了,我们先来看下返回基础类型的情况:
@ModelAttribute
public String init() {
return "userId";
}
这时候我们调用hello方法的时候,同样可以发现ModelMap中已经有一个值了,但ModelMap中的值是{"string","userId"}
,可以看到,如果ModelAttribute没有配置name的话,那么默认情况下将根据方法参数类型或方法返回类型进行推断出来。我们再来看下:
@ModelAttribute
public List<String> init() {
List<String> list = new ArrayList<>();
list.add("123");
return list;
}
同样,可以看到ModelMap中的值是{"stringList", List对象}
;
@ModelAttribute
public User users() {
User user = new User();
user.setId("123");
return user;
}
同样,可以看到ModelMap中的值是{"user", Object}
;
接下来,我们设置ModelAttribute的name属性,以上面的List对象为例,再来看下ModelMap对象中的值:
@ModelAttribute(name = "userIdList")
public List<String> init() {
List<String> list = new ArrayList<>();
list.add("123");
return list;
}
可以看到这次ModelMap中的键值对的键值是userIdList
;
(3)现在我们来看下用于方法中的参数上的情况:
第一种是单个参数的情况:
@ModelAttribute(name = "id")
public String init() {
return "123";
}
@RequestMapping("/hello.html")
public String hello(ModelMap modelMap, @ModelAttribute("id") String id) {
System.out.println(id);
System.out.println(modelMap);
return "hello";
}
这时候,我们发送请求:http://localhost:8080/hello.html?id=456
,我们可以看到,由于ModelMap中已经有了名称为id的对象,所以直接从里面取出绑定到参数id上,而没有取客户端过来的值,这时候id的值是123;
我们稍微修改下,再来看一下:
@RequestMapping("/hello.html")
public String hello(ModelMap modelMap, @ModelAttribute String id) {
System.out.println(id);
System.out.println(modelMap);
return "hello";
}
由于没有配置ModelAttribute的name属性,那么默认将根据参数的类型来取值,也就是名称为string
的对象。我们发送请求:http://localhost:8080/hello.html?string=456
,这时候ModelMap中没有,然后从客户端取值,值为456,绑定到参数id上,然后自动绑定到ModelMap中。
这时候来看下第二种情况,参数是对象的情况:
@RequestMapping(value = "/test2.html", method = RequestMethod.GET)
public String test2(@ModelAttribute("user1") User user, ModelMap modelMap) {
System.out.println(user);
System.out.println(modelMap);
return "hello";
}
@ModelAttribute("user1")
public User getUser() {
User user = new User();
user.setId("123");
user.setName("name1");
return user;
}
这时候,我们发送请求:/world/test2.html?id=345
,可以看下此时对象user和数据模型ModelMap中的值:
User{name='name1', id='345', aDouble=null}
可以看到,对象user中的值被覆盖为了345。
(4)再来看一下ModelAttribute和RequestMapping一起使用的情况:
@RequestMapping("/hello.html")
@ModelAttribute("attribute")
public String hello(ModelMap modelMap, @ModelAttribute(name = "id") String id) {
System.out.println(id);
System.out.println(modelMap);
return "hello";
}
这时候该方法的返回值并不仅仅是一个视图,并且还是ModelMap中对象名称为attribute
的值,这时候我们在页面上使用${attribute}
可以看到,正常打印出了hello
。
该参数一般情况下也是配合@ControllerAdvice用于全局的配置。
网友评论