介绍–什么是JSR303
JSR 303是Java中的一项规范,用于定义在Java应用程序中执行数据校验的元数据模型和API。JSR 303的官方名称是"Bean Validation",它提供了一种在Java对象级别上执行验证的方式,通常用于确保输入数据的完整性和准确性。
JSR 303中最常见的用法是使用注解在Java Bean上添加验证规则。以下是一些常用的注解:
此实现与 Hibernate ORM 没有任何关系。 JSR 303 用于对 Java Bean 中的字段的值进行验证。
Spring MVC 3.x 之中也大力支持 JSR-303,可以在控制器中对表单提交的数据方便地验证。
注:可以使用注解的方式进行验证
JSR 303 基本的校验规则
空检查
- @Null 验证对象是否为null
- @NotNull 验证对象是否不为null, 无法查检长度为0的字符串
- @NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0, 只对字符串, 且会去掉前后空格.
- @NotEmpty 检查约束元素是否为NULL或者是EMPTY.
Booelan检查
- @AssertTrue 验证 Boolean 对象是否为 true
- @AssertFalse 验证 Boolean 对象是否为 false
其他校验
@Size(min, max): 检查值的长度是否在指定范围内。
@Min(value): 检查数字值是否大于等于指定值。
@Max(value): 检查数字值是否小于等于指定值。
@Pattern(regex): 使用正则表达式检查字符串值。
@Email: 检查字符串是否为有效的电子邮件地址等。
在实体类或者vo类使用验证规则,可以大幅度减轻数据校验的规范性价比。
具体使用
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
对需要使用校验的字段添加注解
信息可以进行传值设置,不然就是默认值
@NotEmpty(message = "品牌名必须填写")
@Schema(description = "品牌名")
private String name;
默认信息如下
在控制层开启校验
在控制层开启该注解@Validated即可开启校验
public Result<String> update(@Validated @RequestBody PmsBrandVO vo){
pmsBrandService.update(vo);
return Result.ok();
}
自定义校验失败
上诉的步骤中,校验失败,不满足条件的会抛出异常,所以为了和前端配合需要进行自定义异常处理,返回前端一个json,而不是服务端抛出异常
BindException
在需要返回json的校验bean后跟,BindingResult,校验绑定结果类,对异常进行处理
public Result<String> save(@Validated({AddGroup.class}) @RequestBody PmsBrandVO vo, BindingResult result){
//
// 形参单个添加这个可以,但是批量很难实现,为此需要自定义异常处理
// 需要取求掉该注解,才可以将异常抛出
if (result.hasErrors())
{
String message = result.getFieldErrors().stream()
.map(fieldError -> {
// 获取到错误字段
String field = fieldError.getField();
// 获取到自定义的错误消息提示
String errMessage = fieldError.getDefaultMessage();
// 返回拼接的错误消息字符串
return field + ":" + errMessage;
})
.collect(Collectors.joining(", ")); // 将错误消息用逗号分隔
log.info("错误消息:{}",message);
// 创建一个 Result 对象,将错误消息传递给它
return Result.error(message);
}else {
pmsBrandService.save(vo);
return Result.ok();
}
但是上诉只样也只是对单个控制器校验校验为此,需要自己,定义异常处理结果
自定义异常处理器 处理校验失败异常
1.把控制层的结构异常结果绑定类进行删除,让控制器将异常进行抛出
2.自定义异常处理
/**
* 异常处理器
* 用于集中处理所有异常情况,并确保返回给前端的是处理过的 JSON 信息而不是异常信息。
*/
@Slf4j
@RestControllerAdvice//监听rescontroller的增强方法 advice增强 对应还有controllelrAdvice
public class ServerExceptionHandler {
/**
* 处理自定义异常
* @param ex 抛出的自定义异常
* @return 包含异常信息的 Result 对象
*/
@ExceptionHandler(ServerException.class)//捕获的异常类型
public Result<String> handleException(ServerException ex) {
return Result.error(ex.getCode(), ex.getMsg());
}
/**
* 处理 Spring MVC 参数绑定、Validator 校验不正确的异常
* @param ex 抛出的绑定异常
* @return 包含异常信息的 Result 对象
*/
@ExceptionHandler(BindException.class)
public Result<String> bindException(BindException ex) {
FieldError fieldError = ex.getFieldError();
assert fieldError != null;
return Result.error(fieldError.getDefaultMessage());
}
/**
* 处理访问被拒绝的异常
* @param ex 抛出的访问被拒绝异常
* @return 包含异常信息的 Result 对象
*/
@ExceptionHandler(AccessDeniedException.class)
public Result<String> handleAccessDeniedException(Exception ex) {
return Result.error(ErrorCode.FORBIDDEN);
}
/**
* 未知异常类型,用于处理未捕获的其他异常情况
*/
@ExceptionHandler(Exception.class)
public Result<String> handleException(Exception ex) {
log.error(ex.getMessage(), ex);
return Result.error(ErrorCode.INTERNAL_SERVER_ERROR);
}
}
这样就可以做到统一处理,异常结果
高级功能
自定义校验注解
如果现有的异常处理结果满足不了我们对字段的需求,那么可以进行自定义校验注解,在自定义校验注解之前,我们需要了解自定义校验注解
自定义注解
注解有些元注解,以及生命周期都是很简单的概念,这里讲一下大致
自定义注解主要通过@interface关键字来定义。
自定义注解的组成包括:
注解声明:使用@interface关键字。
元注解(Meta-annotations):
用于注解其他注解的注解。常用的元注解有@Target、@Retention、- @Documented和@Inherited。
- @Target:指定注解可以应用的Java元素类型(如METHOD, FIELD等)。
- @Retention:指定注解在哪一个级别可用,生命周期(源代码中(SOURCE)、类文件中(CLASS)或运行时(RUNTIME)),而我们定义的大部分注解都是在运行时候,用来进行操作日志保存和权限校验
注解体:定义注解的属性。
其他的注解关键字,点开任意注解都可以了解大概
演示
我这里定义自定义注解,模拟操作前进行的日志保存
首先启动开启注解功能
@EnableAspectJAutoProxy
/**
* 所有的Annotation 会自动继承java.lang.Annotation这一接口,并且不能再去继承别的类或是接口。
*/
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Prelog {
String message() default "执行前先打印的日志信息";
}
在我的业务控制器上添加注解
好了现在我的注解定义好了,并且让我的方法使用上了注解,但是这样注解是没办法知道我们的业务逻辑的,
所以需要实现他的逻辑,这里运用到了aop详细了解aop思想,大概就是对目标做增强,在不改变源码的基础上
引入aop的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
定义一个切面类,对我自定义注解进行逻辑实现
@Aspect
@Component
@Slf4j
public class PrelogAspect {
//切点:使用LogAnnotation注解标识的方法都进行切入,也可以使用通配符配置具体要切入的方法名
@Pointcut("@annotation(com.mall.Annotation.Prelog)")
public void pointCut(){
}
//环绕通知
/**
* 在AOP中,joinPoint.proceed()方法用于继续执行切入点处的原始方法。换句话说,它实际上调用了被切入的方法,无论是类的构造函数、方法或字段初始化等。
* 在你的情况下,你的切点是使用@Prelog注解标识的方法,因此当切点匹配到一个被@Prelog注解标记的方法时,joinPoint.proceed()方法会执行该方法。因此,在@Around通知中,joinPoint.proceed()执行的就是被@Prelog注解标记的方法。
* 所以,Object jsonResult = joinPoint.proceed();这行代码实际上执行了被@Prelog注解标记的方法,并将其结果存储在jsonResult变量中。这个变量可以在切面中进一步处理或返回给调用方。
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("pointCut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
try {
System.out.println("开始执行注解逻辑");
// 获取目标方法签名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取目标方法
Method method = signature.getMethod();
// 获取注解值
Prelog annotation = method.getAnnotation(Prelog.class);
// 获取属性
String message = annotation.message();
System.out.println(message);
// 执行目标方法
Object jsonResult = joinPoint.proceed(); // 执行方法
return jsonResult;
} catch (Exception e) {
e.printStackTrace();
throw e; // 抛出异常
}
}
}
访问被标记注解的接口
当然自定义注解配合aop还可以做权限校验,访问接口前判断是否有对应权限
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.stereotype.Component;
@Aspect
@Component
public class PermissionAspect {
@Around("@annotation(CheckPermission)")
public Object checkPermission(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
CheckPermission annotation = signature.getMethod().getAnnotation(CheckPermission.class);
String permission = annotation.value();
// 这里模拟权限校验逻辑
if (!hasPermission(permission)) {
throw new SecurityException("没有权限执行此操作");
}
return joinPoint.proceed(); // 执行原方法
}
private boolean hasPermission(String permission) {
/**
*根据该用户在系统的上下文 对照是否拥有该权限
**/
// 模拟权限校验逻辑,实际中应替换为具体的校验逻辑
// 例如,检查当前用户是否拥有该权限
// 这里简单模拟总是返回true
return true;
}
}
好了大概了解自定义注解和aop的原理进行实现,自定义校验数据的注解
自定义校验注解实现
1.定义校验注解 我这里定义的是判断字段的值是否是我设置的集合中的值
/**
* 1.编写一个自定义注解作用于多个元素校验
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {OptionListConstraint.class })//3.指定使用什么校验器 可以指定多个校验器
public @interface OptionList {
/**
* 1.1改造validate注解的基本属性
* 注解的类型只能是基本类型和布尔值以及枚举,字符串
* @return
*/
//2.定义自己需要的注解属性
int[] values() default {0,1};//默认该注解的这个属性是0,1
String message() default "必须提交指定的数值";//原min注解的默认消息定义在租界中华这个
Class<?>[] groups() default { };
//做校验的时候自定义负载参数
Class<? extends Payload>[] payload() default { };
}
2.对注解进行实现
jsr中的注解进行注解校验都是通过实现校验器接口实现的,点开@Constrain注解
所以需要实现该接口
/**
* 2.对校验器进行重写
* 实现该接口的俩个方法
* 参数<校验注解,校验对象类型>
* 就是自定义注解中的逻辑实现
*/
public class OptionListConstraint implements ConstraintValidator<OptionList, Integer> {
private Set<Integer> set= new HashSet<Integer>();
//初始化方法
// 参数为校验注解
// 可以在该方法中获取校验注解中的属性值
@Override
public void initialize(OptionList constraintAnnotation) {
//1.得到赋于注解的数值
int[] values = constraintAnnotation.values();
//2.将数值赋值给set
for (int i : values) {
set.add(i);
}
}
/**
* 判断是否校验成功
* @param value object to validate 需要校验的对象 也就是赋值的属性字段的值
* @param context context in which the constraint is evaluated 上下文对象
*
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
// 判断当前的值是否在set集合中
return set.contains(value);
}
}
校验分组
这个行为类似于范围限制,比如,对于id主键字段,我们在新增时候是需要前端不传递的,修改又需要前端传递,为此对与不同状态进行分组处理
- 定义俩个接口 表示不同组别 无需写什么方法
2.限定字段校验范围,那些字段是什么组别的时候进行校验
3.控制层 对应校验赋值时标明组别 注意注解是@Validated 不是@Valid
关于自定义注解
要详细理解 AOP(面向切面编程)、自定义注解的实现以及它们背后的逻辑,我们需要从基本概念、原理理解。
一、AOP(面向切面编程)基础
1. AOP 是什么?
AOP 是面向切面编程(Aspect-Oriented Programming),它的核心思想是将一些与业务无关但又需要频繁使用的功能(如日志记录、事务管理、权限验证等)从业务代码中分离出来,以减少代码重复,并提高代码的可维护性。
AOP 的主要作用是通过 切面(aspect) 在方法调用的不同阶段插入额外的功能逻辑,而无需修改方法本身。这样我们可以在方法执行的前后或者抛出异常时执行一些特定的逻辑。
2. AOP 的关键概念
- 切面(Aspect):一个切面是封装的关注点,类似于实现一项功能模块。比如日志记录就是一个切面。
- 连接点(Join Point):方法执行的某个点,比如方法调用之前或之后。一个连接点是程序执行的一个具体位置,Spring AOP 支持对方法执行的连接点进行操作。
- 切入点(Pointcut):用于定义在哪些连接点上切面将被应用,比如所有带有特定注解的方法。
- 通知(Advice):指在切入点处要执行的代码,分为前置通知、后置通知、环绕通知等类型。
- 目标对象(Target Object):即被代理的对象,切面会对目标对象的方法进行增强。
- 代理(Proxy):AOP 底层通过动态代理机制来实现,Spring AOP 使用 JDK 动态代理或 CGLIB 动态代理来实现方法增强。
- AOP 在 Spring 中的实现
Spring 通过代理模式实现 AOP 功能。Spring AOP 有两种代理方式:
JDK 动态代理:适用于目标对象实现了接口的情况。
CGLIB 动态代理:适用于目标对象没有实现接口的情况。CGLIB 通过生成目标类的子类来进行代理。
二、自定义注解与 AOP 结合
自定义注解是 AOP 实现的常用方式之一,它通过注解为方法增加元数据,在运行时解析这些注解并执行相应的增强逻辑。
- 自定义注解的实现步骤
定义注解:注解用于标注那些需要被 AOP 切入的点。我们可以通过(元注解) @Retention 和 @Target 来定义注解的保留策略和适用范围。
@Retention(RetentionPolicy.RUNTIME):表示注解在运行时可用。
@Target(ElementType.METHOD):表示注解应用于方法上。
示例:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope {
String value() default "";
}
定义切面:切面用于定义增强逻辑,配合 AOP 的通知类型,如 @Before、@After 等。我们可以通过切入点表达式(Pointcut Expression)来定义在何处应用切面逻辑。
切面的实现:
@Aspect
@Component
public class DataScopeAspect {
@Before("@annotation(controllerDataScope)")
public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable {
// 清理之前的作用域信息
clearDataScope(point);
// 处理新的数据范围
handleDataScope(point, controllerDataScope);
}
private void clearDataScope(JoinPoint point) {
// 清理数据逻辑
}
private void handleDataScope(JoinPoint point, DataScope controllerDataScope) {
// 处理数据逻辑
}
}
- JoinPoint 的作用
JoinPoint 是 AOP 中的核心接口,它提供了对当前正在执行的目标方法和上下文的访问。通过 JoinPoint,我们可以获取目标方法的信息(如方法名称、方法参数等)并进行操作。
常用 API:
- joinPoint.getSignature():获取方法签名(包含方法名称、返回类型等)。
- joinPoint.getArgs():获取目标方法的参数。
- joinPoint.getTarget():获取目标对象。
在 doBefore() 方法中,我们可以通过 JoinPoint 操作方法的参数,或者执行一些逻辑来控制数据范围等操作。
- 过滤器链的类比
AOP 和过滤器链的相似性,这里进一步解释两者的联系和区别。
过滤器链:过滤器链是 HTTP 请求在进入 Controller 之前被一系列 Filter 处理的过程,适用于 Servlet 容器中的请求处理机制。每个 Filter 都可以在请求处理之前、之后或者阻止请求继续进行。
AOP:则作用在方法级别,更加灵活。AOP 可以在方法调用前后插入逻辑,不仅仅是 HTTP 请求,还可以对任何符合切入点的 Java 方法进行增强。
尽管 AOP 和过滤器链的思想类似,都是在某个执行过程前后插入逻辑,但 AOP 具有更高的灵活性,因为它不仅仅局限于 HTTP 请求。
三、AOP 的执行时机
Spring AOP 提供了多个通知类型来控制增强逻辑的执行时机:
@Before:目标方法执行之前执行的通知。
@After:目标方法执行结束后执行,无论方法是成功返回还是抛出异常。
@AfterReturning:目标方法成功返回后执行,可以获取方法的返回值。
@AfterThrowing:目标方法抛出异常时执行,可以捕获异常。
@Around:环绕通知,在目标方法执行前后都可以执行,最为灵活,可以控制方法是否执行,修改方法参数或返回值。(只有该方法可以控制注解表明的方法是执行还是拦截)
例子
假设我们有一个业务方法需要在执行前后插入日志逻辑,可以用 @Around 来实现。
@Around("execution(* com.example.service.MyService.*(..))")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object proceed = joinPoint.proceed(); // 执行目标方法
long executionTime = System.currentTimeMillis() - start;
//模拟操作后日志
System.out.println(joinPoint.getSignature() + " executed in " + executionTime + "ms");
return proceed;
}
在这个例子中,logExecutionTime 会在目标方法执行前记录开始时间,执行方法后记录执行时间,最后将执行时间打印出来。
四、Spring AOP 的底层机制
1. 动态代理
Spring AOP 使用动态代理来实现方法增强。动态代理通过生成目标对象的代理类,在代理类的方法中插入增强逻辑。
JDK 动态代理:当目标类实现了接口时,Spring 使用 JDK 动态代理,通过接口创建代理类。
CGLIB 动态代理:当目标类没有实现接口时,Spring 使用 CGLIB 创建目标类的子类,并在子类中插入增强逻辑。
2. 切面在 Spring 中的配置
在 Spring 中,AOP 切面是通过 @Aspect 注解来声明的。Spring 通过扫描 @Aspect 注解来识别哪些类是切面,并根据切入点(注解标记的方法或者字段)表达式将通知应用到目标对象上。
五、完整的实现逻辑流程
- 定义注解:通过自定义注解(如 @DataScope)标注需要增强的方法。
- 切面类:通过 @Aspect 声明一个切面类,使用通知(如 @Before、@Around)定义在不同方法执行阶段的逻辑。
- 切入点表达式:使用切入点表达式(如 @annotation(controllerDataScope))来指定增强逻辑应该应用在哪些方法上。
- 代理生成:Spring AOP 在运行时根据目标对象生成代理对象,通过代理对象的方法执行增强逻辑。
- 执行目标方法:代理对象根据切面类中定义的通知,在目标方法执行前后插入增强逻辑。
注意事项
很多人接触到aop是为了实现自定义权限注解校验,有些人使用before,有些人使用aourd通知,
那么俩者有什么区别呢?
下面演示校验"sys:order:update"权限字段
before
这个实现方式类似过滤器,在请求击中对应的controller前进行处理,如果没有对应的权限字段就抛出异常,让整个请求链 失效不再处理达到权限校验的结果
切面代码
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.JoinPoint;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class PermissionAspectBefore {
// Before注解:在方法执行之前进行权限校验
@Before("@annotation(requiresPermission)")
public void checkPermissionBefore(JoinPoint joinPoint, RequiresPermission requiresPermission) throws Throwable {
String requiredPermission = requiresPermission.value(); // 获取注解中要求的权限
// 获取用户的权限信息,模拟的用户权限,实际可从数据库、缓存获取
String[] userPermissions = getCurrentUserPermissions();
// 如果没有所需权限,抛出异常,阻止方法执行
if (!hasPermission(userPermissions, requiredPermission)) {
throw new SecurityException("No permission to access this resource.");
}
}
// 模拟获取当前用户的权限
private String[] getCurrentUserPermissions() {
return new String[] { "sys:order:create", "sys:order:view" }; // 假设用户没有 "sys:order:update"
}
// 检查用户是否拥有特定权限
private boolean hasPermission(String[] userPermissions, String requiredPermission) {
for (String permission : userPermissions) {
if (permission.equals(requiredPermission)) {
return true;
}
}
return false;
}
}
aroud方法实现
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class PermissionAspectAround {
// Around注解:环绕方法执行
@Around("@annotation(requiresPermission)")
public Object checkPermissionAround(ProceedingJoinPoint point, RequiresPermission requiresPermission) throws Throwable {
String requiredPermission = requiresPermission.value(); // 获取注解中的权限值
// 获取用户的权限信息
String[] userPermissions = getCurrentUserPermissions();
// 如果用户没有权限,返回错误信息,不执行目标方法
if (!hasPermission(userPermissions, requiredPermission)) {
return "No permission to access this resource.";
}
// 如果有权限,继续执行目标方法
return point.proceed();
}
// 模拟获取当前用户权限
private String[] getCurrentUserPermissions() {
return new String[] { "sys:order:create", "sys:order:view" }; // 假设用户没有 "sys:order:update"
}
// 检查用户是否拥有特定权限
private boolean hasPermission(String[] userPermissions, String requiredPermission) {
for (String permission : userPermissions) {
if (permission.equals(requiredPermission)) {
return true;
}
}
return false;
}
}