欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Springboot 表单数据校验、数据转换、利用AOP和SPEL实现分布式锁

程序员文章站 2022-05-03 10:23:23
...

个人笔记,不一定正确!!!

一、表单数据校验

a、用户输入的数据,后端必须校验,校验数据是否为空、格式以及必须遵守的业务规则

b、自定义数据校验器

1、定义实体类User,包含name、age、birthday三个属性

package com.cn.dl.bean;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * Created by yanshao on 2019/2/19.
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private String name;
    private Integer age;
    private Date birthday;
}

2、针对User定义的校验器UserValidator

package com.cn.dl.validator;

import com.cn.dl.bean.User;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

/**
 * 自定义Spring的Validator验证接口
 * Created by yanshao on 2019/2/19.
 */
@Component
public class UserValidator implements Validator {
    /**
     * 判断是否支持校验当前实体类
     * @param aClass
     * */
    @Override
    public boolean supports(Class<?> aClass) {
        return User.class.equals(aClass);
    }

    @Override
    public void validate(Object obj, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors,"name","Name is empty");
        ValidationUtils.rejectIfEmpty(errors,"age","Age is empty");
        User user = (User) obj;
        if(user.getAge() != null && (user.getAge() < 0 || user.getAge() > 100)){
            errors.rejectValue("age", "Age value is illegal");
        }
    }
}

这里实现了Validator,Validator有两个方法:

 boolean supports(Class<?> var1);
 void validate(Object var1, Errors var2);

support(Class<?> var1): 用来判断当前校验类型是否为需要校验的实体类,也就是说只有User.class.equals(var1) == true,才会调用validate(Object var1,Errors var2)

validate(Object var1,Errors var2):具体的校验规则

3、在controller中绑定校验器BaseController

package com.cn.dl.controller;

import com.cn.dl.validator.UserValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.lang.Nullable;
import org.springframework.validation.DataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RestController;

/**
 * Created by yanshao on 2019/2/19.
 */
@RestController
public class BaseController {

    @Autowired
    private UserValidator userValidator;

    @InitBinder
    public void userValidatorBinder(DataBinder dataBinder){
        try {
            dataBinder.setValidator(userValidator);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

}
package com.cn.dl.controller;

import com.cn.dl.bean.User;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

/**
 * Created by yanshao on 2019/2/19.
 */
@RestController
@RequestMapping({"/api/user"})
public class UserController extends BaseController{

    @PostMapping("/register")
    public String register(@Valid User user, BindingResult bindingResult){
        System.out.println("User >>>> " + user.toString());
        while (bindingResult.hasErrors()){
            for(ObjectError objectError : bindingResult.getAllErrors()){
                return objectError.getCode();
            }
        }
        return "register success!";
    }
//    @PostMapping("/personTest")
//    public String personTest(@Valid Person person, BindingResult bindingResult){
//        System.out.println("person >>>> " + person.toString());
//        while (bindingResult.hasErrors()){
//            for(ObjectError objectError : bindingResult.getAllErrors()){
//                return objectError.getCode();
//            }
//        }
//        return "personTest success!";
//    }


}

校验器中用到了这个工具类ValidationUtils,提供好几个非空校验的方法,以及返回的errorCode、errorMsg

Springboot 表单数据校验、数据转换、利用AOP和SPEL实现分布式锁

ValidationUtils.rejectIfEmpty(errors,"name","Name is empty");
ValidationUtils.rejectIfEmpty(errors,"age","Age is empty");

 在方法请求参数中申明校验实体类并在请求结束之后返回errorCode

public String register(@Valid User user, BindingResult bindingResult){
        System.out.println("User >>>> " + user.toString());
        while (bindingResult.hasErrors()){
            for(ObjectError objectError : bindingResult.getAllErrors()){
                return objectError.getCode();
            }
        }
        return "register success!";
}

效果:

Springboot 表单数据校验、数据转换、利用AOP和SPEL实现分布式锁

Springboot 表单数据校验、数据转换、利用AOP和SPEL实现分布式锁

Springboot 表单数据校验、数据转换、利用AOP和SPEL实现分布式锁

       对于表单数据的校验,hibernate-validator包中提供的就已经足够用来,除非一些特殊需要,前段时间因为业务上的需要,搞了一个对嵌套的list集合的校验。

二、数据转换

      比如User中age为Integer类型,而在调用的时候并没有指明类型,Spring容器专门有这种类型之间的转换方法,我们自己也可以自定义数据转换器。

Spring的转换器SPI

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.core.convert.converter;

import org.springframework.lang.Nullable;

@FunctionalInterface
public interface Converter<S, T> {
    @Nullable
    T convert(S var1);
}

比如我们自定义一个将dateString 转换为java.util.Date

package com.cn.dl;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.core.convert.converter.Converter;
public class StringDate implements Converter<String, Date>{

    // 日期转换格式
    private String pattern;
    public StringDate(String pattern) {
        this.pattern = pattern;
    }
    @Override
    public Date convert(String arg0) {  
        SimpleDateFormat sd = new SimpleDateFormat(pattern);
        try {
            return sd.parse(arg0);
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }

}

也可以配置全局日期和时间格式

package com.cn.dl.config;

import org.springframework.boot.SpringBootConfiguration;
import org.springframework.format.datetime.DateFormatter;
import org.springframework.format.datetime.DateFormatterRegistrar;
import org.springframework.format.number.NumberFormatAnnotationFormatterFactory;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

/**
 * 自定义全局日期和时间格式
 * WebMvcConfigurationSupport:自定义或者指定一些Web MVC 的配置,比如拦截器的配置
 * Created by yanshao on 2019/2/19.
 */
@SpringBootConfiguration
public class InitConfig extends WebMvcConfigurationSupport {

    /**
     * 这样配置之后,所有时间格式都会以yyyyMMdd来解析
     * 其实费了半天劲,还不如在bean上使用@DateTimeFormat (pattern = "yyyyMMdd") 对每不同字段,可以用不同的时间格式
     * */
    @Override
    public FormattingConversionService mvcConversionService() {
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);
        conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());
        DateFormatterRegistrar registrar = new DateFormatterRegistrar();
        //设置日期格式
        registrar.setFormatter(new DateFormatter("yyyyMMdd"));
        registrar.registerFormatters(conversionService);
        return conversionService;
    }

}
 

WebMvcConfigurationSupport:自定义或者指定一些Web MVC 的配置,比如拦截器的配置,这样配置之后,所有时间格式都会以yyyyMMdd来解析

最后的效果:

Springboot 表单数据校验、数据转换、利用AOP和SPEL实现分布式锁

Springboot 表单数据校验、数据转换、利用AOP和SPEL实现分布式锁

其实费了半天劲,还不如在bean上使用@DateTimeFormat (pattern = "yyyyMMdd") ,针对每不同字段,可以用不同的时间格式

 @DateTimeFormat (pattern = "yyyyMMdd")
 private Date birthday;

三、利用AOP和SPEL实现分布式锁

      在分布式项目中,用户触发插入、更新等操作,我们只需要其中一个服务执行,如果不加分布式锁,后果很严重。

     1、SPEL表达式

       Spring Expression Language(简称“SpEL”)是一种强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于  Unified EL,但提供了其他功能,最值得注意的是方法调用和基本字符串模板功能。虽然还有其他几种Java表达式语言--OGNL,MVEL和JBoss EL,仅举几例 - 创建Spring表达式语言是为Spring社区提供一种支持良好的表达式语言,可以在所有产品中使用春季组合。其语言特性受Spring组合项目要求的驱动,包括基于Eclipse的Spring Tool Suite中代码完成支持的工具要求。也就是说,SpEL基于技术无关的API,可以在需要时集成其他表达式语言实现。

package com.cn.dl.spel;

import com.cn.dl.bean.User;
import org.junit.Test;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

import java.lang.reflect.Method;

/**
 * Created by yanshao on 2019/2/19.
 */
public class SPELDemo {

    @Test
    public void spelTest1() {
        ExpressionParser parser = new SpelExpressionParser();
        Expression exp = parser.parseExpression("'Hello World'");
        String message = (String) exp.getValue();
        System.out.println(message);
    }

    /**
     * spel支持广泛的功能,例如调用方法,访问属性和调用构造函数。
     * */
    @Test
    public void spelTest2(){
        ExpressionParser parser = new SpelExpressionParser();
        Expression exp = parser.parseExpression("'Hello World'.concat('!')");
        String message = (String) exp.getValue();
        System.out.println(message);
    }

    @Test
    public void spelTest3(){
        ExpressionParser parser = new SpelExpressionParser();
        Expression exp = parser.parseExpression("'Hello World'.bytes.length");
        int length = (Integer) exp.getValue();
        System.out.println(length);
        Expression exception =  parser.parseExpression("T(java.lang.Math).random() * 100.0");
        System.out.println(exception.getValue());
    }

    @Test
    public void spelTest4(){
        User user = User.builder()
                .name("yanshao")
                .age(24)
                .build();
        ExpressionParser parser = new SpelExpressionParser();
        Expression exp = parser.parseExpression("'com:cn:dl:lock:'+ name");
        System.out.println(exp.getValue(user));
    }

    @Test
    public void spelTest5() throws NoSuchMethodException {
        String name = "yanshao";
        Integer age = 23;
        Method method = SPELDemo.class.getMethod("register",String.class,Integer.class);
        ParameterNameDiscoverer discover = new DefaultParameterNameDiscoverer();
        String[] parameterNames = discover.getParameterNames(method);
        for(String parameterName : parameterNames){
            System.out.println("参数名 >>> " + parameterName);
        }
        Object[] args = {name ,age};
        ParameterEvaluationContext evaluationContext = new ParameterEvaluationContext(
                new LocalVariableTableParameterNameDiscoverer(),method, args , SPELDemo.class);

        ExpressionParser parser = new SpelExpressionParser();
        Expression exp = parser.parseExpression("'com:cn:dl:lock:'+ #name");
        System.out.println(exp.getValue(evaluationContext));
    }
    @Test
    public void spelTest6() throws NoSuchMethodException {
        String name = "yanshao";
        Integer age = 23;
        Method method = SPELDemo.class.getMethod("register",String.class,Integer.class);
        ParameterNameDiscoverer discover = new DefaultParameterNameDiscoverer();
        String[] parameterNames = discover.getParameterNames(method);
        for(String parameterName : parameterNames){
            System.out.println("参数名 >>> " + parameterName);
        }
        Object[] args = {name ,age};
        UserEvaluationContext userEvaluationContext = new UserEvaluationContext(method,args);
        ExpressionParser parser = new SpelExpressionParser();
        Expression exp = parser.parseExpression("'user:cn:dl:lock:'+ #name");
        System.out.println(exp.getValue(userEvaluationContext));
    }

    public void register(String name,Integer age){
        System.out.println("name >>>" + name + "," + "age >>> "+ age);
    }

}

SPEL几个使用案例

2、通过AOP和SPEL表达式实现分布式锁的方法

Reids锁实现:

  • 分布锁一般通过来redis实现,主要通过setnx函数向redis保存一个key,value等于保存时的时间戳,并设置过期时间,然后返回true;
  • 当获得锁超过等待时间返回false;
  • 通过key获取redis保存的时间戳,如果value不为空,并且当前时间戳减去-value值超过锁过期时间返回false;
  • 如果一次没有获得锁,则每隔一定时间(10ms或者20ms)再获取一次,直到超过等待时间返回false。

定义切面类,连接点就是在指定的方法上加了@RedisLockValidator ,例如:

public @interface RedisLockValidator {
    //key值
    String redisKey() default "";
}
@Around("@annotation(redisLockValidator)")
@PostMapping("/register")
@RedisLockValidator(redisKey = "'user:cn:dl:lock:'+ #user.userId")
public String register(@Valid User user, BindingResult bindingResult){}

3、具体实现

a、定义注解RedisLockValidator

package com.cn.dl.annotation;

import java.lang.annotation.*;

/**
 * Created by yanshao on 2019/2/20.
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RedisLockValidator {
    //key值
    String redisKey() default "";
}

b、定义UserEvaluationContext来解析方法上的参数并生成redisKey

package com.cn.dl.spel;

import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;

import java.lang.reflect.Method;

/**
 * 使用EvaluationContext接口表示上下文对象,用于设置根对象、自定义变量、自定义函数、
 * 类型转换器等,StandardEvaluationContext是EvaluationContext的子类
 * 使用StandardEvaluationContext来解析方法上的参数,需要lookupVariable方法,
 * 然后调用exp.getValue时,会调用lookupVariable在EvaluationContext上下文中寻找key=name的value
 * Created by yanshao on 2019/2/20.
 */
public class UserEvaluationContext extends StandardEvaluationContext {

    private Method method;
    private Object args[];

    public UserEvaluationContext(Method method,Object args[]){
        this.method = method;
        this.args = args;
    }


    @Nullable
    @Override
    public Object lookupVariable(String name) {
        //在寻找参数值之前,需要将键值对set到EvaluationContext上下文对象
        //private final Map<String, Object> variables = new ConcurrentHashMap();
        String[] parameterNames = new DefaultParameterNameDiscoverer().getParameterNames(method);
        if (parameterNames != null) {
            for (int i = 0; i < parameterNames.length; i++) {
                setVariable(parameterNames[i], this.args[i]);
            }
        }
        return super.lookupVariable(name);
    }



}

        这里继承了StandardEvaluationContext,StandardEvaluationContext是EvaluationContext的子类 ,EvaluationContext接口用于设置根对象、自定义变量、自定义函数、 类型转换器等,使用StandardEvaluationContext来解析方法上的参数,需要重写lookupVariable方法,  然后在调用exp.getValue时,会在EvaluationContext上下文中寻找key=name的value

String[] parameterNames = new DefaultParameterNameDiscoverer().getParameterNames(method);
      if (parameterNames != null) {
         for (int i = 0; i < parameterNames.length; i++) {
           setVariable(parameterNames[i], this.args[i]);
      }
 }

       在lookupVariable方法中,我们需要将方法的参数名和值set到variables中,在StandardEvaluationContext类中,定义了这个map集合

private final Map<String, Object> variables = new ConcurrentHashMap();

c、RedisLockAspect

package com.cn.dl.redislock;

import com.cn.dl.annotation.RedisLockValidator;
import com.cn.dl.spel.UserEvaluationContext;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * Created by yanshao on 2019/2/20.
 */
@Aspect
@Component
public class RedisLockAspect {

    @Around("@annotation(redisLockValidator)")
    public Object redisLockTest(ProceedingJoinPoint proceedingJoinPoint, RedisLockValidator redisLockValidator) throws Throwable {
        MethodSignature methodSignature = (MethodSignature)proceedingJoinPoint.getSignature();
        //获取加了@RedisLockValidator的方法
        Method method = methodSignature.getMethod();
        //参数
        Object args[] = proceedingJoinPoint.getArgs();
        //定义传入方法的上下文参数并解析最终的key
        UserEvaluationContext userEvaluationContext = new UserEvaluationContext(method,args);
        ExpressionParser parser = new SpelExpressionParser();
        Expression exp = parser.parseExpression(redisLockValidator.redisKey());
        String redisKey = (String) exp.getValue(userEvaluationContext);
        System.out.println("redisKey >>>> " + redisKey);
        // TODO: 2019/2/20 redis中判断是否已经存在当前key,如果已经存在,直接退出
//       if(! redis.get(reidskey)){
//            Object object = proceedingJoinPoint.proceed();
//            return object;
//        }
//        return null;
        Object object = proceedingJoinPoint.proceed();
        return object;
    }

}

d、UserContoller再修改一点

package com.cn.dl.controller;

import com.cn.dl.annotation.RedisLockValidator;
import com.cn.dl.bean.User;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

/**
 * Created by yanshao on 2019/2/19.
 */
@RestController
@RequestMapping({"/api/user"})
public class UserController extends BaseController{

    @PostMapping("/register")
    @RedisLockValidator(redisKey = "'user:cn:dl:lock:'+ #user.userId")
    public String register(@Valid User user, BindingResult bindingResult){
        System.out.println("User >>>> " + user.toString());
        while (bindingResult.hasErrors()){
            for(ObjectError objectError : bindingResult.getAllErrors()){
                return objectError.getCode();
            }
        }
        return "register success!";
    }

}

对于SPEL这种写法,需要在官网上再看看

@RedisLockValidator(redisKey = "'user:cn:dl:lock:'+ #user.name")

e、实体类User也要修改

package com.cn.dl.bean;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * Created by yanshao on 2019/2/19.
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private String name;
    private Long userId;
    private Integer age;
    private Date birthday;
}

效果:

Springboot 表单数据校验、数据转换、利用AOP和SPEL实现分布式锁

控制台打印的日志

2019-02-20 14:42:12.978  INFO 21424 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2019-02-20 14:42:12.990  INFO 21424 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 12 ms
redisKey >>>> user:cn:dl:lock:13240115
User >>>> User(name=xiaoming, userId=13240115, age=0, birthday=Sun Jun 19 00:00:00 CST 1994)

生成的redisKey >>>> user:cn:dl:lock:13240115,这样我们就可以通过这个key来实现分布式锁了。