文章目录
前言
体能状态先于精神状态,习惯先于决心,聚焦先于喜好
入参校验的必要性
属性校验在实际开发中是一个非常常见的场景,尤其在web服务和对外提供接口服务时,对入参对合法性校验是确保程序安全的第一道关卡.
校验注解
Spring Validation 验证框架对参数的验证机制提供了@Validated(Spring’s JSR-303规范,是标准JSR-303的一个变种)
javax提供了@Valid(标准JSR-303规范)
二者配合BindingResult可以直接提供参数验证结果。
@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制。@Valid:作为标准JSR-303规范,还没有吸收分组的功能。
常见的校验相关注解和含义
需要强调的是,注解只是一种标注,其自身除了携带一些基本信息外,无法自动完成校验功能,一般需要借助于相应的处理逻辑
- @Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上
- @Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上
- @NotEmpty(message=“提示信息”):用在集合类上面;不能为null,而且长度必须大于0
- @NotBlank(message=“提示信息”): 用在String上面;只能作用在String上,不能为null,而且调用trim()后,长度必须大于0
- @NotNull(message=“提示信息”):用在基本类型上;不能为null,但可以为empty
- @Length(min=, max=,message="") : 只适用于String 类型
- @Size(min=,max=,message=""):只适用于整形
- @AssertTrue: 验证 Boolean 对象是否为 true
- @AssertFalse: 验证 Boolean 对象是否为 false
- @Past: 验证 Date 和 Calendar 对象是否在当前时间之前
- @Future: 验证 Date 和 Calendar 对象是否在当前时间之后
- @Pattern: 验证 String 对象是否符合正则表达式的规则
- @Min: 验证 Number 和 String 对象是否大等于指定的值
- @Max: 验证 Number 和 String 对象是否小等于指定的值
- @DecimalMax: 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
- @DecimalMin: 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
- @Digits: 验证 Number 和 String 的构成是否合法
- @Digits(integer=,fraction=): 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。
常规校验的两种方式及不足
Web 应用与 @Validated、@Valid 、BindingResult
在 Web 应用中我们可以选择使用这种方式对入参进行校验.
- 案例代码
Person 为实体类,请参考本文末代码段 Person.java
@RequestMapping("/demo")
public String demo(@Validated Person person,BindingResult result) {
if(result.hasErrors()){
List<ObjectError> ls=result.getAllErrors();
for (int i = 0; i < ls.size(); i++) {
System.out.println("error:"+ls.get(i));
return "error";
}
}
return "success";
}
手动校验字段合法性
另一种方法是手动对入参进行校验,这个就比较灵活类,比如为入参提供一个单独对校验方法,入下面的代码
- 案例代码
/**
* 校验 Person 字段的格式合法性
* @param person
* @return
*/
private boolean checkPerson(Person person) {
if(StringUtils.isBlank(person.getName())){
return false;
}
return true;
}
两种方式的不足
WEB 应用的校验方式被局限在 WEB应用中了,第二种手动处理的方式则无法统一处理校验逻辑,并且没有应用注解——显而易见的规则描述.
多想一步:借助注解自己写校验的逻辑
再次强调,注解虽然可以携带信息,但是注解本身并不能自动完成校验功能.
思路阐述
1、利用 Spring AOP 将需要进行校验的方法设置为切点
2、使用 Java 反射机制,检验入参的成员变量是否有被注解修饰
3、对于存在注解修饰的成员变量的值和注解的规则是否匹配
4、对于满足校验条件的不做处理,返回正常的状态信息,对于不满足条件的,抛出异常
5、对于异常进行统一处理,返回错误状态信息
代码 UML 设计
实体类 Person.java
实体类:ResponseBody.java,用于接口方法统一的返回值-便于正常情况和异常情况的统一处理
接口:TemService
类:TemServiceImpl:TemService的实现类
类:MyAspect,切面定义类,对 TemService 的 sayHello 方法设置为切点
测试类:AspectTest
配置文件:applicationContext-tem.xml
关门,放代码
Person.java
package com.bestcxx.stu.tem.aspect.dto;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.apache.commons.lang3.builder.ToStringBuilder;
/**
* 员工类
* @author wujie
*/
public class Person {
/**姓名*/
@NotNull(message = "name不可为空")
private String name;
/**年龄*/
@Size(min = 1,max=200,message = "age应在1到200之间")
private int age;
/**备注*/
private String comments;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getComments() {
return comments;
}
public void setComments(String comments) {
this.comments = comments;
}
@Override
public String toString() {
/*
* 查看对方包的 maven依赖:https://mvnrepository.com/artifact/org.apache.commons/commons-lang3/3.9
**/
return ToStringBuilder.reflectionToString(this);
}
}
ResponseBody.java
package com.bestcxx.stu.tem.aspect.dto;
import java.io.Serializable;
import org.apache.commons.lang3.builder.ToStringBuilder;
/**
* 承接结果的实体
* @author wujie
*
*/
public class ResponseBody implements Serializable{
private static final long serialVersionUID = 1L;
private String code;
private String msg;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
@Override
public String toString() {
/*
* 查看对方包的 maven依赖:https://mvnrepository.com/artifact/org.apache.commons/commons-lang3/3.9
**/
return ToStringBuilder.reflectionToString(this);
}
}
TemService.java
package com.bestcxx.stu.tem.aspect.service;
import com.bestcxx.stu.tem.aspect.dto.Person;
import com.bestcxx.stu.tem.aspect.dto.ResponseBody;
/**
* 定义接口
* @author wujie
*/
public interface TemService {
ResponseBody sayHello(Person p);
}
TemServiceImpl.java
package com.bestcxx.stu.tem.aspect.serviceimpl;
import org.springframework.stereotype.Component;
import com.bestcxx.stu.tem.aspect.dto.Person;
import com.bestcxx.stu.tem.aspect.dto.ResponseBody;
import com.bestcxx.stu.tem.aspect.service.TemService;
@Component
public class TemServiceImpl implements TemService {
@Override
public ResponseBody sayHello(Person p) {
System.out.println("进入 sayHello");
ResponseBody r=new ResponseBody();
r.setCode("SUCCESS");
r.setMsg("调用成功");
return r;
}
}
MyAspect.java
package com.bestcxx.stu.tem.aspect;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import com.bestcxx.stu.tem.aspect.dto.Person;
import com.bestcxx.stu.tem.aspect.dto.ResponseBody;
/**
* 自定义切面对指定切点对入参的成员变量注解进行合规校验
* 切点对应的方法有全局返回值-成功/失败是同一个类的对象,状态码区分成功或失败
* @author wujie
*/
@Aspect
@Component
public class MyAspect {
/**
* 定义切点
* @param point
* @return
*/
@Pointcut("execution(* com.bestcxx.stu.tem.aspect.service.TemService.sayHello(..))")
public void point() {
}
/**
* 切面的处理方法
* @param point
* @return
*/
@Around(value="point()")
public Object around(ProceedingJoinPoint point) {
System.out.println("来自@aspect的@Around注解的通知:这是一个切点");
try {
//获取第一个参数
//本切面拦截的方法只有一个入参 Person
Object object0=point.getArgs()[0];
//本例子作为测试方法,限定了对 Person类 的校验,如果需要通用方法可以去除本if方法
if(object0 instanceof Person) {
//方法内部对 object0 对应对注解和成员变量值对约定进行匹配
checkFieldsByObject(object0);
}
//正常逻辑不受影响
Object result = point.proceed();
return result;
}catch(Throwable t) {
//如果上面的校验抛出了异常,在这里进行捕获
ResponseBody r=new ResponseBody();
r.setCode("ERROR");
r.setMsg(t.getMessage());
return r;
}
}
/**
* 检测一个 Object 对象的注解和值是否匹配
* @param object0 被检测的对象
* @throws Exception
*/
private void checkFieldsByObject(Object object0) throws Exception{
//获取该类的成员变量
Field[] fields=Person.class.getDeclaredFields();
//对成员变量进行遍历
for(Field f:fields) {
//指定对象的 指定成员方法 检测 Field
checkAnnotationByField(object0,f);
}
}
/**
* @param object0 被检测的对象
* @param f Field 对应的成员变量
* @throws Exception
*/
private void checkAnnotationByField(Object object0,Field f) throws Exception {
//需要考虑一个属性同时被多个注解修饰到情况
Annotation[] annotations=f.getAnnotations();
for(Annotation annotation:annotations) {
//指定对象、注解、成员变量的具体检测
checkFiledValueByAnnotationType(object0,annotation,f);
}
}
/**
* 具体的不同的注解采取不同的处理逻辑
* @param object0 被检测的对象
* @param annotation 对应的注解类别
* @param f Field 对应的成员变量
* @throws Exception
*/
private void checkFiledValueByAnnotationType(Object object0,Annotation annotation,Field f) throws Exception {
//@NotNull 注解判断
if(annotation instanceof NotNull) {
//允许操作 private 修饰的成员变量
f.setAccessible(true);
//判断该入参的属性是否为 null
if(f.get(object0)==null) {
throw new Exception(((NotNull) annotation).message());
}
}
//@Size 注解判断
if(annotation instanceof Size) {
//允许操作 private 修饰的成员变量
f.setAccessible(true);
if(((Size) annotation).min()>f.getInt(object0)||((Size) annotation).max()<f.getInt(object0)){
throw new Exception(((Size) annotation).message());
}
}
}
}
applicationContext-tem.xml
务必注意这里需要 aop:aspectj-autoproxy/,否则 Spring AOP 功能无法奏效
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:orm="http://www.springframework.org/schema/orm"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.3.xsd
http://www.springframework.org/schema/orm http://www.springframework.org/schema/orm/spring-orm-4.3.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd
"
>
<!--开启AOP功能-->
<aop:aspectj-autoproxy/>
<!--启动bean扫描注册和注解注入-->
<context:component-scan base-package="com.bestcxx.stu.tem.aspect"/>
</beans>
测试类 AspectTest.java
package com.bestcxx.stu.tem.aspect;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.bestcxx.stu.tem.aspect.dto.Person;
import com.bestcxx.stu.tem.aspect.dto.ResponseBody;
import com.bestcxx.stu.tem.aspect.service.TemService;
@DirtiesContext
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:test/applicationcontext-tem.xml"})
//@TransactionConfiguration(transactionManager = "defaultTransactionManager",defaultRollback=false)//事务管理
//@Rollback(true)
public class AspectTest {
@Autowired
private TemService temService;
@Test
public void testSayHello() {
Person p=new Person();
//p.setAge(220);
p.setName("wj");
p.setComments("SUCCESS 校验通过,ERROR校验不通过");
ResponseBody rb=temService.sayHello(p);
System.out.println("接口返回结果:"+rb.toString());
}
}
- 校验通过的结果
来自@aspect的@Around注解的通知:这是一个切点
进入 sayHello
接口返回结果:com.bestcxx.stu.tem.aspect.dto.ResponseBody@73e9cf30[code=SUCCESS,msg=调用成功]
- 校验不通过的结果
来自@aspect的@Around注解的通知:这是一个切点
接口返回结果:com.bestcxx.stu.tem.aspect.dto.ResponseBody@73e9cf30[code=ERROR,msg=age应在1到200之间]
jdk代理还是cglib代理?
在xml文档中,我们设置了开启了AOP的内容
<aop:aspectj-autoproxy/>
这种情况下,Spring 在使用切面功能时,如果切面是一个类,并且这个类实现类接口,那么Spring会使用jdk动态代理进行处理.如果这个类没有实现接口,Spring就会使用cglib进行动态代理.
这里隐藏着一个问题,就是一旦一个类实现类接口,并且开启AOP功能,但是在声明的时候以类进行声明应用,那么在<aop:aspectj-autoproxy/>
情况下,会报错,说找不到这个类
这个时候需要手动开启cglib代理<aop:aspectj-autoproxy proxy-target-class="true"/>
jdk动态代理和cglib动态代理的区别在于jdk只能代理接口,cglib可以代理类
参考连接
[1]、https://www.cnblogs.com/thinkjava/p/7073599.html
[2]、https://www.jianshu.com/p/9e33ec934ff0
[3]、https://blog.csdn.net/bestcxx/article/details/79145859
[4]、https://blog.csdn.net/bestcxx/article/details/88645684
[5]、https://blog.csdn.net/bestcxx/article/details/52826058