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

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

一、表单数据校验

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

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!";
}

效果:

       对于表单数据的校验,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来解析

最后的效果:

其实费了半天劲,还不如在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;
}

效果:

控制台打印的日志

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来实现分布式锁了。

 

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

燕少༒江湖

给我一份鼓励!谢谢!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值