• 正说PropertyValuesProvider的应用


    • Github地址:https://github.com/andyslin/spring-ext
    • 编译、运行环境:JDK 8 + Maven 3 + IDEA + Lombok
    • spring-boot:2.1.0.RELEASE(Spring:5.1.2.RELEASE)
    • 如要本地运行github上的项目,需要安装lombok插件

    在上篇文章从SpringMVC获取用户信息谈起中,由一个典型的应用场景说起,通过分析SpringMVC的源码,引入新接口PropertyValuesProvider,我们给SpringMVC的参数绑定提供了一种新的机制,姑且称之为PropertyValuesProvider机制。在这篇文章中,就来说一下这种新机制的几个应用,这些应用都是我在实际工作中曾经遇到的。

    一、准备工作

    为了后面的测试,先做一些准备工作:

    1. 创建一个SpringBoot应用,添加maven依赖:

       <dependency>
       	<groupId>org.springframework.boot</groupId>
       	<artifactId>spring-boot-starter-web</artifactId>
       </dependency>
      
       <dependency>
       	<groupId>org.springframework.boot</groupId>
       	<artifactId>spring-boot-starter-test</artifactId>
       </dependency>
      
    2. 添加启动类

      @SpringBootApplication
      public class ArgsBindApplication {
         public static void main(String[] args) {
            SpringApplication.run(ArgsBindApplication.class, args);
         }
      }
      
    3. 添加测试类,启用MockMvc

      @RunWith(SpringRunner.class)
      @SpringBootTest
      @AutoConfigureMockMvc
      public class ArgsBindApplicationTests {
      
         @Autowired
         private MockMvc mvc;
      }
      

    二、验证PropertyValuesProvider机制

    先纯粹的验证一下PropertyValuesProvider机制,不预设具体的应用场景。添加一个PropertyValuesProvider的实现类,并注册为Spring容器中的Bean

    @Component
    public class TestPropertyValuesProvider implements PropertyValuesProvider {
    
        @Override
        public void addBindValues(MutablePropertyValues mpvs, ServletRequest request, Object target, String name) {
            mpvs.add("beforeBindProperty", "beforeBindPropertyValue");
        }
    
        @Override
        public void afterBindValues(PropertyAccessor accessor, ServletRequest request, Object target, String name) {
            if (target instanceof TestForm) {
                accessor.setPropertyValue("afterBindProperty", "afterBindPropertyValue");
            }
        }
    }
    

    这个实现类逻辑非常简单,就是在SpringMVC原生的参数绑定和验证之前,提供了一个候选的属性beforeBindProperty,在参数绑定和验证之后,又修改了目标对象的属性afterBindProperty。当然,为了确保有afterBindProperty这个属性,实现类中先对目标对象做了一个类型判断,在实际应用中,可以做更灵活的处理。目标类型TestForm就是一个简单的POJO:

    @Getter
    @Setter
    @ToString
    public class TestForm {
    
        private String beforeBindProperty;
    
        private String afterBindProperty;
    }
    

    这里没有直接使用@Data注解,是因为@Data功能太多,会生成很多方法,而我只是需要gettersettertoString就可以了。

    然后控制器定义如下:

    @RestController
    public class TestController {
    
        @GetMapping("/test")
        public TestForm test(TestForm form) {
            return form;
        }
    }
    

    最后,在测试类ArgsBindApplicationTests中添加测试方法:

    @RunWith(SpringRunner.class)
    @SpringBootTest
    @AutoConfigureMockMvc
    public class ArgsBindApplicationTests {
    
        @Autowired
        private MockMvc mvc;
    
        // 添加的测试方法
        @Test
        public void test() throws Exception {
            MvcResult result = mvc.perform(MockMvcRequestBuilders.get("/test")).andReturn();
            MockHttpServletResponse response = result.getResponse();
            Assert.assertEquals(200, response.getStatus());
    
            JSONObject json = new JSONObject(response.getContentAsString());
            Assert.assertEquals("beforeBindPropertyValue", json.getString("beforeBindProperty"));
            Assert.assertEquals("afterBindPropertyValue", json.getString("afterBindProperty"));
        }
    }
    

    通过运行测试案例,可以发现实现类PropertyValuesProviderTest已经生效。如果不熟悉使用MockMVC,也可以本地启动应用后,在浏览器或Postman中手工发起请求。

    三、实际应用

    (一)公共的BaseForm

    很多时候,后端的控制器需要根据会话上下文获取一些公共属性(上篇文章中的用户信息就是一种会话上下文信息),如果在每个控制器中去获取,虽然思路简单,但是编写麻烦,更重要的是不便于维护。这时候,我们可以把需要提取的信息定义一个公共的BaseForm,然后具体的业务Form添加一个类型为BaseForm的属性(或者直接继承BaseForm),具体步骤如下:

    1. 定义公共的BaseForm和业务Form
      @Getter
      @Setter
      @ToString
      public class BaseForm {
      
         private String userId;
      
         private String orgId;
      }
      
      @Getter
      @Setter
      @ToString
      public class BusinessForm {
      
         private BaseForm base;
      }
      
    2. 编写PropertyValuesProvider的实现类,并添加@Component注入到Spring容器中:
      @Component
      public class BaseFormPropertyValuesProvider implements PropertyValuesProvider {
      
         @Override
         public void addBindValues(MutablePropertyValues mpvs, ServletRequest request, Object target, String name) {
            mpvs.add("base", obtainBaseForm());
         }
      
         /**
          * 获取BaseForm,这里直接返回,实际应用可能会获取session、解密jwt或者其它逻辑
          *
          * @return
          */
         private BaseForm obtainBaseForm() {
            BaseForm form = new BaseForm();
            form.setUserId("admin");
            form.setOrgId("0000");
            return form;
         }
      

      当然,这里只是演示。实际应用中不宜写死base名称,可以根据Type反过来获取属性名称(可以参考后面的案例),并缓存这些元信息。

    3. 编写控制器Controller
      @RestController
      public class BaseFormController {
      
         @GetMapping("/baseform")
         public BusinessForm test(BusinessForm form) {
            return form;
         }
      }
      
    4. 添加测试方法
      @Test
      public void baseform() throws Exception {
         MvcResult result = mvc.perform(MockMvcRequestBuilders.get("/baseform")).andReturn();
         MockHttpServletResponse response = result.getResponse();
         Assert.assertEquals(200, response.getStatus());
      
         JSONObject json = new JSONObject(response.getContentAsString());
         JSONObject base = json.getJSONObject("base");
      
         Assert.assertEquals("admin", base.getString("userId"));
         Assert.assertEquals("0000", base.getString("orgId"));
      }
      
      测试案例通过,说明已经按预期设置公共属性了。

    (二)配置属性

    除了从会话上下文中获取信息之外,在实际工作中还遇到过一种情况,就是需要根据请求从DB中加载配置,当然,这些逻辑可以放在service层,但是放到service层,除了代码散落各处之外,也不能享有SpringMVC中便利的参数校验机制了。而通过PropertyValuesProvider机制,可以将这些代码像AOP一样收敛到一起(我始终以为,AOP不只是提供了一种实用功能,更重要的还是一种编程思想,学习AOP,除了学习怎么使用,还要学习怎么思考)。

    我们来一起处理这种情形:

    1. 添加用于设别特殊属性的注解:

      @Target({ElementType.FIELD})
      @Retention(RetentionPolicy.RUNTIME)
      public @interface ConfigProperty {
      
         String value();
      }
      

      为了简单,这里做了一些简化,没有区分配置的类型(文件、DB、环境变量等),也没有添加属性前缀匹配等。

    2. 在业务Form的属性中添加注解:

      @Getter
      @Setter
      @ToString
      public class ConfigPropertyForm {
      
         @ConfigProperty("configName")
         private String configProperty;
      }
      
    3. 编写PropertyValuesProvider实现类,实现属性注入逻辑:

      @Component
      public class ConfigPropertyPropertyValuesProvider implements PropertyValuesProvider {
      
         @Override
           public void addBindValues(MutablePropertyValues mpvs, ServletRequest request, Object target, String name) {
               for (Class<?> cls = target.getClass(); !cls.equals(Object.class); cls = cls.getSuperclass()) {
                   for (Field field : cls.getDeclaredFields()) {
                       // 实际应用中可以将是否包括@ConfigProperty注解等元信息缓存起来
                       if (field.isAnnotationPresent(ConfigProperty.class)) {
                           mpvs.add(field.getName(), obtainConfigProperty(field.getAnnotation(ConfigProperty.class), field));
                       }
                   }
               }
           }
      
         /**
           * 根据注解和Field获取属性
           */
           private Object obtainConfigProperty(ConfigProperty configProperty, Field field) {
               String propertyName = configProperty.value();
               // 这里直接返回属性值,实际应用中可以根据注解从环境变量、DB或者缓存中获取
               return propertyName + "Value";
           }
       }
      

      属性是否包含@ConfigProperty注解的元信息可以缓存起来

    4. 添加控制器,在测试类ArgsBindApplicationTests中添加测试方法,运行测试案例:

      @RestController
      public class ConfigPropertyController {
      
        @GetMapping("/configProperty")
        public ConfigPropertyForm test(ConfigPropertyForm form) {
              return form;
        }
      }
      
      @Test
      public void configProperty() throws Exception {
         MvcResult result = mvc.perform(MockMvcRequestBuilders.get("/configProperty")).andReturn();
         MockHttpServletResponse response = result.getResponse();
         Assert.assertEquals(200, response.getStatus());
      
         JSONObject json = new JSONObject(response.getContentAsString());
         Assert.assertEquals("configNameValue", json.getString("configProperty"));
      }
      

    可能有朋友会说,要使用环境属性或者配置,不是直接可以使用Spring提供的@Value注解吗?的确,在ControllerServiceSpring容器中的Bean,可以直接使用@Value注解,但是我们这里是在Form中使用配置。

    回想一下这个案例,这实际上是一种新的模式:定义一种用于设别的注解,在Form对象的属性中使用注解,然后根据注解、属性、请求等设置属性值。 下面再看一个这种模式的应用场景:

    (三)RSA解密

    Web应用中,为了安全考虑,在客户端使用JSjsencrypt将用户输入的密码通过RSA加密,然后传输到服务端,服务端使用SpringMVC的机制接受参数,但是服务端有一个校验(密码长度在6到16位),这样,使用原生的校验机制,被校验的值是RSA加密后的值(很长),因而通不过校验,我们看看这种场景:

    1. 为了使用SpringMVC的校验机制,先在pom.xml中添加依赖:

      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-validation</artifactId>
      </dependency>
      
    2. 添加识别需要RSA解密的标志注解:

      @Target({ElementType.FIELD})
      @Retention(RetentionPolicy.RUNTIME)
      public @interface RsaDecrypt {
      }
      
    3. 定义Form

      @Getter
      @Setter
      @ToString
      public class RsaDecryptForm {
         @Length(min = 6, max = 16, message = "长度只能在6-16位")
         @RsaDecrypt
         private String rsa;
      }
      
    4. 编写Controller,添加校验注解@Validated:

      @RestController
      public class RsaDecryptController {
      
         @GetMapping("/rsaDecrypt")
         public RsaDecryptForm test(@Validated RsaDecryptForm form) {
            return form;
         }
      }
      
    5. 在测试类ArgsBindApplicationTests中添加测试方法,运行测试案例:

      @Test
      public void rsa() throws Exception {
         String src = "abadewew";//原始值
         // 模拟客户端使用RSA加密
         String encrypt = RSAUtils.encryptByPublicKey(src, RSA_PAIR[0]);
         MvcResult result = mvc.perform(MockMvcRequestBuilders.get("/rsaDecrypt").param("rsa", encrypt)).andReturn();
         MockHttpServletResponse response = result.getResponse();
         Assert.assertEquals(200, response.getStatus());
      
         JSONObject json = new JSONObject(response.getContentAsString());
         Assert.assertEquals(src, json.getString("rsa"));
      }
      

      结果发现,第一个断言已经失败,从日志中可以看出抛出了BindException异常,因为没有通过校验:

       MockHttpServletRequest:
           HTTP Method = GET
           Request URI = /rsaDecrypt
           Parameters = {rsa=[envr8Rm72k5c2FxkMjhhPxeHCyvh+IENKTAFO30z6c/dUn8Z3rMv1gyqCAYmaSIy09KH4kFdO90Gsz4uJhzi/riM4bOOBwCcXBvq6J1Md9yiZOgdl/XuDVf7V4IJsE2NUQnhmtfFFJhSOuPzeMJ7HntC1J/CrDUBaL5n40tWW6I=]}
               Headers = {}
                   Body = null
           Session Attrs = {}
      
       Handler:
                   Type = org.autumn.spring.argsbind.rsa.RsaDecryptController
               Method = public org.autumn.spring.argsbind.rsa.RsaDecryptForm org.autumn.spring.argsbind.rsa.RsaDecryptController.test(org.autumn.spring.argsbind.rsa.RsaDecryptForm)
      
       Async:
           Async started = false
           Async result = null
      
       Resolved Exception:
                   Type = org.springframework.validation.BindException
      
       ModelAndView:
               View name = null
                   View = null
                   Model = null
      
       FlashMap:
           Attributes = null
      
       MockHttpServletResponse:
               Status = 400
           Error message = null
               Headers = {}
           Content type = null
                   Body = 
           Forwarded URL = null
       Redirected URL = null
               Cookies = []
      

      为什么会这样呢?这是因为客户端RSA加密之后,传递到服务端的值是加密后的值,长度远远超过16,因而校验失败。现在,我们添加一个PropertyValuesProvider实现类做一下预处理:

    6. 添加RsaDecryptPropertyValuesProvider实现类:

      @Component
      public class RsaDecryptPropertyValuesProvider implements PropertyValuesProvider {
      
         @Override
         public void addBindValues(MutablePropertyValues mpvs, ServletRequest request, Object target, String name) {
               for (Class<?> cls = target.getClass(); !cls.equals(Object.class); cls = cls.getSuperclass()) {
                   for (Field field : cls.getDeclaredFields()) {
                       if (field.isAnnotationPresent(RsaDecrypt.class)) {
                           mpvs.add(field.getName(), obtainConfigProperty(field.getName(), request));
                       }
                   }
               }
         }
      
         /**
           * 根据注解和Field获取属性
           */
           private Object obtainConfigProperty(String name, ServletRequest request) {
               String encrypt = request.getParameter(name);
               if (StringUtils.hasText(encrypt)) {
                   return RSAUtils.decryptByPrivateKey(encrypt, RSA_PAIR[1]);
               }
               return encrypt;
           }
       }
      
    7. 再次运行测试案例,发现已经通过测试,说明将加密传输和原生校验完美结合了!

      这个案例使用了一个工具类RSAUtils,可以从github上查看相关源码,没有任何依赖,只依赖JDK。

    好了,PropertyValuesProvider机制先聊到这,希望对大家有一点点启发,如果你有遇到新的应用场景,也希望能够不惜赐教。

  • 相关阅读:
    Proj THUDBFuzz Paper Reading: PMFuzz: Test Case Generation for Persistent Memory Programs
    入围 WF 后训练记
    算法竞赛历程
    2021 多校 杭电 第十场
    2021 多校 杭电 第九场
    2021 多校 牛客 第十场
    2021 多校 牛客 第九场
    2021 多校 杭电 第八场
    2021 多校 杭电 第六场
    2021 多校 杭电 第七场
  • 原文地址:https://www.cnblogs.com/linjisong/p/11611282.html
Copyright © 2020-2023  润新知