• 正说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机制先聊到这,希望对大家有一点点启发,如果你有遇到新的应用场景,也希望能够不惜赐教。

  • 相关阅读:
    SCAU 12新生赛 H 拥挤的华农校巴
    C#实现让CPU占用率曲线听你的指挥 可指定运行核心
    追MM与设计模式的有趣见解
    FTP操作类
    怎么知道页面被放大缩小了
    SerialPort同步和异步数据读取
    Java Web 应用程序转换为 ASP.NET
    ASP.NET中进行消息处理(MSMQ)
    解压缩文件类
    怎样成为优秀的软件模型设计者?
  • 原文地址:https://www.cnblogs.com/linjisong/p/11611282.html
Copyright © 2020-2023  润新知