Spring/Spring-Boot 学习 使用自定义的ArgumentResolver

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注解,我们需要分别为这四个属性接收idnamegendergrade参数,然后在add方法里面构造Student对象,再调用service方法保存。
这么写的问题主要有:

  1. 如果Student的属性很多,比如有十几个,那么add方法的参数签名就会非常长,且容易出错;
  2. 构造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.javaadd()方法类似下面这样, 直接在形参列表接收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的功能,比如说不是按名称匹配的(不建议),或者我们所组装的对象的某些参数并不包含在requestparameter域中,或许我们需要从request的header中取,又或者我们需要对这个对象添加额外的不是从客户端获取的参数等等。这个时候我们就需要自定义ArgumentResolver,并把它添加到RequestMappingHandlerAdaoterArgumentResolver列表中。这样我们的入参遇到某个类型的参数的时候,可以使用自定义的ArgumentResolver来处理参数的提取和对象的组装。

进阶: 自定义ArgumentResolver

还以Student对象为例,这次我们除了需要id, name, gender, grade四个属性以外,还需要把客户端的类型也保存到Student对象中。客户端的访问类型保存在requestheader中,我们不能通过默认的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实例并添加到HandlerMethodArgumentResolverargumentResolvers列表中。

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的数据,以前缀区分:

查看结果:

posted @ 2019-11-29 14:05  lllunaticer  阅读(1368)  评论(0编辑  收藏  举报