Overview
问题陈述
这一节的示例采用上一篇中的项目 :Spring/Spring-Boot 学习 连接redis数据库
我们看一下这个项目里面的Controller
部分代码,重点看一下它的add
方法是怎么接收客户端传参的:
StudentController.java
@RestController
@RequestMapping("/demo")
public class StudentController {
@Autowired
StudentService studentService;
@PostMapping(path = "/add")
public @ResponseBody String add(@RequestParam String id, @RequestParam String name, @RequestParam String gender, @RequestParam int grade){
studentService.add(new Student(id, name, gender, grade));
return "OK";
}
}
可以看到为了接收从客户端传来的Student
的四个属性,通过@RequestParm
注解,我们需要分别为这四个属性接收id
、name
、gender
、grade
参数,然后在add
方法里面构造Student
对象,再调用service
方法保存。
这么写的问题主要有:
- 如果
Student
的属性很多,比如有十几个,那么add
方法的参数签名就会非常长,且容易出错; - 构造
Student
对象不属于Controller
的业务职责,这样写让Controller
的业务逻辑混乱。
要解决上述的第一个问题,可以用HttpServletRequest
作为add
方法入参,这样可以避免add
方法的签名过长:
@RestController
@RequestMapping("/demo")
public class StudentController {
@Autowired
StudentService studentService;
@PostMapping(path = "/add")
public @ResponseBody String add(HttpServletRequest request){
String id = request.getParameter("id");
String name = request.getParameter("name");
String gender = request.getParameter("gender");
int grade = Integer.parseInt(request.getParameter("grade"));
studentService.add(new Student(id, name, gender, grade));
return "OK";
}
}
这样做可以统一入参,但需要在add
方法中添加提取参数和构造Student
对象的逻辑,没有解决上述的第二个问题,反而使得controller
的业务逻辑更加混乱。
我们希望add
方法可以直接接收Student
对象,不用自己提取参数、组装Student
对象,也就是说,我们希望StudentController.java
的add()
方法类似下面这样, 直接在形参列表接收Student
:
@RestController
@RequestMapping("/demo")
public class StudentController {
@Autowired
StudentService studentService;
@PostMapping(path = "/add")
public @ResponseBody String add(Student student){
studentService.add(student));
return "OK";
}
}
默认的RequestMappingHandlerAdapter
如果你用的是Spring-Boot
,其实它已经帮我们解决了最基础的问题。Spring-Boot
配置了最基础的RequestMappingHandlerAdapter
, 只要在项目的pom
文件中引入了spring-boot-autoconfigure
包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<dependency>
或者直接引入spring-boot-starter
依赖(包含autoconfigrue
依赖)
那么就可以直接像下面这样写controller
的入参,程序可以正常运行:
@RestController
@RequestMapping("/demo")
public class StudentController {
@Autowired
StudentService studentService;
@PostMapping(path = "/add")
public @ResponseBody String add(Student student){
studentService.add(student));
return "OK";
}
}
Spring
容器管理的RequestMappingHandlerAdapter
对象会自动帮我们分解参数并组装成所需要的对象。需要注意的是,Spring
容器自动配置的RequestMappingHandlerAdapter
对象会根据参数的名称(支持驼峰),从客户端的request
的参数域中匹配对应的数据,将参数解析出来并组装成所需要的实例。具体来说,我们上面的add()
方法接收的是Student
对象,但客户端给我们发的request
实际上是一串字符(json
等),这串字符中包含了parameter
标示的部分,是客户端传过来的参数。Spring
容器管理的RequestMappingHandlerAdapter
对象会解析request
字符串,找到parameter
部分,对照我们所需要的Student
对象的属性域(setter方法),用名称匹配的部分来创造Student
对象,返回给add()
方法的入参。RequestMappingHandlerAdapter
对象会按照名称去匹配request
中的参数与add()方法
所需要的Student
对象的参数,并组装Student
对象
这就是spring-boot-autoconfigurer
给我们提供的最基本的RequestMappingHandlerAdapter
, 它完全按照名称匹配且只能组装在request
的参数域中提供参数的对象。
一般情况来说这些就已经足够了。但有时候我们需要增强RequestMappingHandlerAdapter
的功能,比如说不是按名称匹配的(不建议),或者我们所组装的对象的某些参数并不包含在request
的parameter
域中,或许我们需要从request
的header中取,又或者我们需要对这个对象添加额外的不是从客户端获取的参数等等。这个时候我们就需要自定义ArgumentResolver
,并把它添加到RequestMappingHandlerAdaoter
的ArgumentResolver
列表中。这样我们的入参遇到某个类型的参数的时候,可以使用自定义的ArgumentResolver
来处理参数的提取和对象的组装。
进阶: 自定义ArgumentResolver
还以Student
对象为例,这次我们除了需要id
, name
, gender
, grade
四个属性以外,还需要把客户端的类型也保存到Student
对象中。客户端的访问类型保存在request
的header
中,我们不能通过默认的RequestMappingHandlerAdapter
获取到这个信息。
先贴上项目结构:
.
├── java
│ └── com
│ └── example
│ └── accessingredis2
│ ├── AccessingRedis2Application.java
│ ├── configs
│ │ ├── RedisConfiguration.java
│ │ └── StudentArgumentResolver.java
│ ├── controller
│ │ └── StudentController.java
│ ├── dao
│ │ └── StudentRepository.java
│ ├── entity
│ │ └── Student.java
│ └── service
│ └── StudentService.java
└── resources
└── application.properties
说明: 上面的所有类中,凡是在下面没有提到的,都与上篇博客中的代码完全相同: Spring/Spring-Boot 学习 连接redis数据库
首先为Student.java
增加客户端字段agent
:
Student.java
@Data
@AllArgsConstructor
@RedisHash("Student")
public class Student implements Serializable {
private String id;
private String name;
private String gender;
private int grade;
private String agent;
}
然后,编写自定义的ArgumentResolver
StudentArgumentResolver.java
public class StudentArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterType().equals(Student.class);
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
String id = nativeWebRequest.getParameter("id");
String name = nativeWebRequest.getParameter("grade");
String gender = nativeWebRequest.getParameter("gender");
String agent = nativeWebRequest.getHeader("User-Agent");
int grade = Integer.parseInt(Objects.requireNonNull(nativeWebRequest.getParameter("grade")));
return new Student(id, name, gender, grade, agent);
}
}
说明: 自定义的ArgumentResolver
需要实现HandlerMethodArgumentResolver
接口,并重写两个方法:
supportsParameter()
方法用于指定这个自定义的ArgumentResolver
会处理什么样的入参对象,只有这个方法返回true
才会进行下面的resolveArgument()
方法的处理;这里我们判断入参是不是Student
,如果是返回true
交给楼下的resolveArgument()
方法来读取request
组装Student
对象。resolveArgument()
方法读取request
,并按自己写的逻辑来组装Student
对象。注意这里我们除了通过getParameter
方法获取先前四个基本的属性外,还通过getHeader()
方法获得了包含在Header
中的访问客户端的信息,并将其也组装进Student
对象。
配置StudentArgumentResolver
类到HandlerMethodArgumentResolver
中:
RedisConfiguration.java
@Configuration
public class RedisConfiguration extends WebMvcConfigurationSupport {
@Bean
JedisConnectionFactory jedisConnectionFactory(){
RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration();
standaloneConfiguration.setHostName("localhost");
standaloneConfiguration.setPort(6379);
return new JedisConnectionFactory(standaloneConfiguration);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(){
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(jedisConnectionFactory());
return template;
}
@Override
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new StudentArgumentResolver());
}
}
这个配置类就是之前的RedisConfiguration
类,为了将StudentArgumentResolver
类配置到HandlerMethodArgumentResolver
中,需要配置类继承WebMvcConfigurationSupport
类,并重写addArgumentResolvers
方法。在这个方法里,生成StudentArgumentResolver
实例并添加到HandlerMethodArgumentResolver
的argumentResolvers
列表中。
StudentController.java
@RestController
@RequestMapping("/demo")
public class StudentController {
@Autowired
StudentService studentService;
@PostMapping(path = "/add")
public @ResponseBody String add(Student student){
studentService.add(student);
return "OK";
}
@PostMapping("/getStudent")
public Student getStudent(@RequestParam String id){
return studentService.find(id);
}
@GetMapping("/deleteAll")
public String deleteAll(){
studentService.deleteAll();
return "OK";
}
@GetMapping("/getAll")
public List<Student> getAll(){
return studentService.getAll();
}
@PostMapping("/update")
public boolean update(Student student){
boolean flag = studentService.update(student);
return flag;
}
@PostMapping("/delete")
public boolean delete(@RequestParam String id){
return studentService.delete(id);
}
}
说明: Controller
类的add()
方法和update()
方法的入参直接使用Student
对象,并且不需要在这里面写组装Student
对象的逻辑。
测试结果
发请求: 注意请求参数中并不包含agent信息:
查看结果:
结果显示保存了上次请求的客户端是PostMan
再进阶 -- 使用注解来玩更多的花样
保持代码的可读性
上面的StudentArgumentResolver
类中,为了标示我们的ArgumentResolver
能处理哪种入参,我们在supportsParameter()
方法中指定了Student.class
类,这样我们自定义的ArgumentResolver
类会去处理所有Student
类的入参。
这么做没有功能上的问题,但是对代码的可维护性和可读性却很差。为什么?试想,一个新同事看到了StudentController
类中的add()
方法如下:
@PostMapping(path = "/add")
public @ResponseBody String add(Student student){
studentService.add(student);
return "OK";
}
他会可能会以为我们采取了默认的HandlerMethodArgumentResolver
的处理方法,他很难知道我们背后有一套自己的处理入参的逻辑, 他甚至会以为我们这块在Student
类上还少了一个@RequestParam
注解,这样他就很难理解我们的代码,甚至是写代码的人过了一段时间都会忘了这里自己曾经写过的逻辑。也就是说,上面的代码隐藏了我们自己写的逻辑。代码可读性高的一个重要的要求是,尽可能在代码中给出提示,不要隐藏自己写的逻辑
我们可以利用注解来暴露这块的处理逻辑,注解天然适合作代码提示。
首先我们定义一个注解:
Stu_Bean.java
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Stu_Bean {
}
说明: 我们规定了这个注解的使用范围是参数,保留时间是Runtime。
然后,我们在StudentArgumentResolver
的类中,改用注解来标示我们自定义的StudentArgumentResolver
可以处理哪种入参:
StudentArgumentResolver.java
public class StudentArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.hasParameterAnnotation(Stu_Bean.class);
// return methodParameter.getParameterType().equals(Student.class);
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
String id = nativeWebRequest.getParameter("id");
String name = nativeWebRequest.getParameter("name");
String gender = nativeWebRequest.getParameter("gender");
String agent = nativeWebRequest.getHeader("User-Agent");
int grade = Integer.parseInt(Objects.requireNonNull(nativeWebRequest.getParameter("grade")));
return new Student(id, name, gender, grade, agent);
}
}
说明: 注意上面的supportParameter()
方法中指定可处理入参类的逻辑从刚才的判断入参是否是Student类
:
return methodParameter.getParameterType().equals(Student.class)
变成了验证入参是否带有Stu_Bean
注解:
return methodParameter.hasParameterAnnotation(Stu_Bean.class);
在改变了supportParameter()
的验证方式后,我们使用的时候,只要在需要自己的处理逻辑的入参上打上@Stu_Bean
注解,就可以调用自定义的StudentArgumentResolver
来处理入参:
StudentController.java
@RestController
@RequestMapping("/demo")
public class StudentController {
@Autowired
StudentService studentService;
@PostMapping(path = "/add")
public @ResponseBody String add(@Stu_Bean Student student){
studentService.add(student);
return "OK";
}
@PostMapping("/update")
public boolean update(@Stu_Bean Student student){
boolean flag = studentService.update(student);
return flag;
}
}
再次启动项目,效果与之前的处理方式完全一致!但是别人在阅读这个StudentController
的代码的时候,却可以根据@Stu_Bean
注解入参这块有我们自己的处理逻辑。
实际上,之所以有这篇博文的诞生,也归功于组里大哥在写代码的时候用了注解的方式来提醒入参的类做了自己的处理逻辑。我作为一个新人在阅读代码的时候发现了这个注解,很不理解,一步步深挖下去,才发现了这块的自己定义的ArgumentResolver
,才知道了自己处理入参的逻辑。如果不是这个我当时不理解的注解,这块我可能一直都搞不明白。这就是优秀的代码习惯!
利用注解支持多个入参
上面的add()
方法每次只能添加一个Student
对象,如果我们每次要添加多个Student
对象呢?
我们在StudentController
中添加新的方法addTwo()
,使得可以一次添加两个Student
对象。
@PostMapping("/addTwo")
public String addTwo(Student student1, Student student2){
return "OK";
}
要这么做,在request
传过来的时候就必须使用某种方式区分两个Student
对象的数据。这里我们约定,在request
里面student1
的数据带有stu1-
的前缀,student2
的数据带stu2-
的前缀。
我们需要改写Stu_Bean
注解,让它带一个值:
Stu_Bean.java
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Stu_Bean {
public String value();
}
然后需要改写StudentArgumentResolver
的处理逻辑,让它根据参数的前缀的不同来构造不同的对象。
StudentArgumentResolver.java
public class StudentArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.hasParameterAnnotation(Stu_Bean.class);
// return methodParameter.getParameterType().equals(Student.class);
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
Stu_Bean stu_bean = methodParameter.getParameterAnnotation(Stu_Bean.class);
assert stu_bean != null;
//获取注解值
String annotation = stu_bean.value();
String id = nativeWebRequest.getParameter(annotation+"id");
String name = nativeWebRequest.getParameter(annotation+"name");
String gender = nativeWebRequest.getParameter(annotation+"gender");
int grade = Integer.parseInt(Objects.requireNonNull(nativeWebRequest.getParameter(annotation+"grade")));
String agent = nativeWebRequest.getHeader("User-Agent");
return new Student(id, name, gender, grade, agent);
}
}
说明: 注意获取注解值的逻辑
最后在StudentController
中添加addTwo()
方法并打上注解:
@RestController
@RequestMapping("/demo")
public class StudentController {
@Autowired
StudentService studentService;
@PostMapping(path = "/add")
public String add(@Stu_Bean("stu-") Student student){
studentService.add(student);
return "OK";
}
@PostMapping("/addTwo")
public String addTwo(@Stu_Bean("stu1-") Student student1, @Stu_Bean("stu2-") Student student2){
studentService.add(student1);
studentService.add(student2);
return "OK";
}
@PostMapping("/getStudent")
public Student getStudent(@RequestParam String id){
return studentService.find(id);
}
@GetMapping("/deleteAll")
public String deleteAll(){
studentService.deleteAll();
return "OK";
}
@GetMapping("/getAll")
public List<Student> getAll(){
return studentService.getAll();
}
@PostMapping("/update")
public boolean update(@Stu_Bean("stu-") Student student){
boolean flag = studentService.update(student);
return flag;
}
@PostMapping("/delete")
public boolean delete(@RequestParam String id){
return studentService.delete(id);
}
}
测试
请求addTwo()
方法,一次发送两个Student
的数据,以前缀区分:
查看结果: