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