文章目录
AOP是面向切面的横向的编程,在不影响原来业务的基础上,实现动态的增强。
通知顺序:
当方法符合切点规则不符合环绕通知的规则时候,执行的顺序如下
@Before→@After→@AfterRunning(如果有异常→@AfterThrowing)
当方法符合切点规则并且符合环绕通知的规则时候,执行的顺序如下
@Around→@Before→@After→@Around执行 ProceedingJoinPoint.proceed() 之后的操作→@AfterRunning(如果有异常→@AfterThrowing)
先环绕放行前,前置,环绕放行后,后置@After,最终@AfterReturning(对方法的结果做处理)
一、实现AOP方式:
1.使用原生的spring API接口。
2.自定义类,把这个类作为切面,其中的方法在切点前或后执行。
二、具体操作
1.首先,要使用AOP需要导入包
二选一
<!-- 1.原始的aop依赖-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
<!-- 2.springboot的AOP依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.具体三种方式的实现。
第一种:使用原生的spring API接口。
public class Log implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println(o.getClass() + "的" + method.getName() + "方法被调用了");
}
}
第二种:自定义切面类
可以定义多个注解,但是切入面只能有一个,可以有很多个不同切入点的环绕通知
切入点的定义:https://blog.csdn.net/guo_guo_cai/article/details/78332099
将这个类作为切面。其中的方法作为切点前或后执行的方法。
/**
* Aspect 切面
* 日志切面
*/
@Aspect
@Component
public class LogAspect {
/**
* slf4j日志
*/
private final static Logger logger = LoggerFactory.getLogger(LogAspect.class);
/**
* Pointcut 切入点
* 匹配cn.controller包下面的所有方法
*/
@Pointcut("execution(public * cn.controller.*.*(..))")
public void webLog(){}
/**
* 环绕通知
*/
@Around(value = "webLog()")
public Object arround(ProceedingJoinPoint pjp) {
try {
logger.info("1、Around:方法环绕开始.....");
Object o = pjp.proceed();
logger.info("3、Around:方法环绕结束,结果是 :" + o);
return o;
} catch (Throwable e) {
logger.error(pjp.getSignature() + " 出现异常: ", e);
return Result.of(null, false, "出现异常:" + e.getMessage());
}
}
/**
* 方法执行前
*/
@Before(value = "webLog()")
public void before(JoinPoint joinPoint){
logger.info("2、Before:方法执行开始...");
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert attributes != null;
HttpServletRequest request = attributes.getRequest();
// 记录下请求内容
logger.info("URL : " + request.getRequestURL().toString());
logger.info("HTTP_METHOD : " + request.getMethod());
logger.info("IP : " + request.getRemoteAddr());
logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
}
/**
* 方法执行结束,不管是抛出异常或者正常退出都会执行
*/
@After(value = "webLog()")
public void after(JoinPoint joinPoint){
logger.info("4、After:方法最后执行.....");
}
/**
* 方法执行结束,增强处理
*/
@AfterReturning(returning = "ret", pointcut = "webLog()")
public void doAfterReturning(Object ret){
// 处理完请求,返回内容
logger.info("5、AfterReturning:方法的返回值 : " + ret);
}
/**
* 后置异常通知
*/
@AfterThrowing(value = "webLog()")
public void throwss(JoinPoint joinPoint){
logger.error("AfterThrowing:方法异常时执行.....");
}
}
第三种:配合注解使用(常用的)
package com.example.exceptiondemo.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 控制器参数判断注解
*
* @author lc
* @version 1.0
* @date 2021/11/19 18:16
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ParameterJudgeAnnotation {
boolean isVaild() default true;
}
package com.example.exceptiondemo.aop;
import com.example.exceptiondemo.exception.ResultCodeEnum;
import com.example.exceptiondemo.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
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;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* 参数判断你的aop
*
* @author lc
* @version 1.0
* @date 2021/11/19 18:18
*/
@Aspect
@Component
@Slf4j
public class ParameterJudgeAop {
/**
* 环绕通知处理
*
* @param pjp
* @return
* @throws Throwable
*/
@Around("@annotation(com.example.exceptiondemo.aop.ParameterJudgeAnnotation)")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
String reg = "^[\u4e00-\u9fa5a-zA-Z0-9-]+$"; // 只允许中文数字英文和负号
// 获取方法签名
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Object[] args = pjp.getArgs();
// 对参数进行处理
args = Arrays.stream(args).map(arg -> {
Optional<Object> argOptional = Optional.ofNullable(arg);
if (!argOptional.isPresent() || !arg.toString().matches(reg)) {
throw new ServiceException(ResultCodeEnum.BAD_REQUEST);
}
if (arg.toString().contains("省")) {
String s = this.filterPro(arg.toString());
arg = s;
}
return arg;
}).toArray(Object[]::new);
// 放行
return pjp.proceed(args);
}
public String filterPro(String province) {
if (province.contains("省")) {
return province.substring(0, province.length() - 1);
}
return province;
}
}
三、aop已实现的一些参数校验注解 - validation
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
3.1 使用
3.1.1 第一种方式,pojo作为传参的形式
Pojo 定义验证规则
以pojo作为参数传参
(1)User pojo类 定义校验规则
@Data
class User{
@NotNull(message = "用户id不能为空", groups = UpdateUser.class)
private Integer id;
@NotNull(message = "姓名不能为空" ,groups = {CreateUser.class, UpdateUser.class})
private String name;
@NotNull(message = "性别不能为空")
private String sex;
@Max(value = 20 ,message = "最大长度为20")
private String address;
@Email(message = "不满足邮箱格式")
private String email;
@AssertTrue(message = "字段为true才能通过")
private boolean isAuth;
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误")
private String mobile;
@Future(message = "时间在当前时间之后才可以通过")
private Date date;
}
(2)让校验规则生效。在Controller类的时候,我们只需要利用 @Validated 注解来实现pojo的校验
@RequestMapping("users")
public ResponseDTO saveUser( @RequestBody @Validated User user){
}
(3)捕捉验证异常。如果参数校验通过,那么就直接执行接口方法,但是如果失败了,我们如何进行处理
MethodArgumentNotValidException是springBoot中进行绑定参数校验时的异常,需要在springBoot中处理
其他需要处理 ConstraintViolationException异常进行处理
一般像异常捕捉的,可以自定义一个异常捕捉Handler,实现异常捕捉后的数据返回
import com.dto.ResponseDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
@RestControllerAdvice
public class GlobalExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
private static int DUPLICATE_KEY_CODE = 1001;
private static int PARAM_FAIL_CODE = 1002;
private static int VALIDATION_CODE = 1003;
/**
* 处理自定义异常
*/
@ExceptionHandler(BizException.class)
public ResponseDTO handleRRException(BizException e) {
logger.error(e.getMessage(), e);
return new ResponseDTO(e.getCode(), e.getMessage());
}
/**
* 方法参数校验
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseDTO handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
logger.error(e.getMessage(), e);
return new ResponseDTO(PARAM_FAIL_CODE, e.getBindingResult().getFieldError().getDefaultMessage());
}
/**
* ValidationException
*/
@ExceptionHandler(ValidationException.class)
public ResponseDTO handleValidationException(ValidationException e) {
logger.error(e.getMessage(), e);
return new ResponseDTO(VALIDATION_CODE, e.getCause().getMessage());
}
/**
* ConstraintViolationException
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseDTO handleConstraintViolationException(ConstraintViolationException e) {
logger.error(e.getMessage(), e);
return new ResponseDTO(PARAM_FAIL_CODE, e.getMessage());
}
/**
* 路径异常
*/
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseDTO handlerNoFoundException(Exception e) {
logger.error(e.getMessage(), e);
return new ResponseDTO(404, "不好意思,路径不存在,请检查路径是否正确");
}
/**
* 所有其他异常捕捉
*/
@ExceptionHandler(Exception.class)
public ResponseDTO handleException(Exception e) {
logger.error(e.getMessage(), e);
return new ResponseDTO(500, "不好意思,系统繁忙,请稍后再试");
}
}
3.1.2 第二种方式,restful风格
Controller 上加上@Validated
参数上加注解
@RestController
@RequestMapping("user/")
@Validated
public class UserController{
@RequestMapping("users)
public ResponseDTO getUser(@RequestParam("userId") @NotNull(message = "用户id不能为空") Long userId){
}
}
四、自定义注解
如果我们想自定义一个验证的注解,那么需要怎么做呢?
定义一个注解
编写一个验证类
使用
(1)我们首先定义一个像上面的@Null 这样的注解
@Documented
// 这里标注该注解是使用在filed和参数上的
@Target({ElementType.PARAMETER, ElementType.FIELD})
// 运行时生效
@Retention(RetentionPolicy.RUNTIME)
// 指定验证的类是哪个 MyValidator 就是第二步做的事情
@Constraint(validatedBy = MyValidator.class)
public @interface MyValid {
// 提供自定义异常的信息,可以指定默认值
String message() default "参数不合法";
// 分组,详细看 上面的介绍
Class<?>[] groups() default {};
// 针对bean的,使用不多
Class<? extends Payload>[] payload() default {};
}
(2)编写一个验证类
我们需要编写一个 实现 ConstraintValidator 类实现类,来指定我们的校验规则
如果不指定特定注解的情况下,直接使用
// 这个是Max的指定的验证规则源码
public abstract class AbstractMaxValidator<T> implements ConstraintValidator<Max, T> {
protected long maxValue;
public AbstractMaxValidator() {
}
public void initialize(Max maxValue) {
this.maxValue = maxValue.value();
}
public boolean isValid(T value, ConstraintValidatorContext constraintValidatorContext) {
if (value == null) {
return true;
} else {
return this.compare(value) <= 0;
}
}
protected abstract int compare(T var1);
}
// 自定义的验证类
public class MyValidator implements ConstraintValidator {
@Override
public void initialize(Annotation constraintAnnotation) {
// 可以获取注解的值 ,一般写在该方法中
}
@Override
public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
// 编写自己属性验证的规则,o 则是待验证的值
return false;
}
}
如果在指定特定注解的情况下,那么我们就可特定 注解
// 自定义的验证类
public class MyValidator implements ConstraintValidator<MyValid , Object> {
@Override
public void initialize(MyValid myValid) {
// 可以获取注解的值 ,一般写在该方法中
}
@Override
public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
// 编写自己属性验证的规则,o 则是待验证的值
return false;
}
}
(3)使用。就跟正常的那些注解一样使用即可。
@Data
public Class Example{
@NotBlank(message = "姓名不能为空")
@MyValid(message = "姓名有误,请核对后提交")
private String name;
}
五、实际操作
5.1 操作日志和错误日志的存储
package net.facelib.eam.plancenter.webController.config.logInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.facelib.eam.EamErrorType;
import net.facelib.eam.exception.PlanException;
import net.facelib.eam.plancenter.common.MyPlanConstant;
import net.facelib.eam.plancenter.webController.config.PermissionAspect;
import net.facelib.eam.plancenter.webController.mapper.LogMapper;
import net.facelib.eam.plancenter.webController.utils.ExceptionLogService;
import net.facelib.eam.plancenter.webController.utils.MyCommonUtil;
import net.faceliib.eam.plancenter.common.entity.PcLog;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Date;
/**
* 日志切面
*
* @author lc
* @version 1.0
* @date 2022/7/8 16:43
*/
@Aspect
@Component
@Order(1)
public class LogAspect {
private Logger logger = LoggerFactory.getLogger(LogAspect.class);
ThreadLocal<Long> threadLocal = new ThreadLocal<>();
@Autowired
private LogMapper logMapper;
@Autowired
private MyCommonUtil myCommonUtil;
@Autowired
private ExceptionLogService exceptionLogService;
@Pointcut("execution(* net.facelib.eam.plancenter.webController.service..*.*(..))")
public void performance() {
}
@Around("@annotation(net.facelib.eam.plancenter.webController.config.logInfo.SaveLog)")
public Object pcLog(ProceedingJoinPoint joinPoint) throws Throwable {
threadLocal.set(System.currentTimeMillis()); // 接口调用时间
// 使用ServletRequestAttributrs请求上线文获取方法信息
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String className = joinPoint.getSignature().getDeclaringTypeName();
String methodName = joinPoint.getSignature().getName();
Object[] paramArgs = joinPoint.getArgs();
ObjectMapper mapper = new ObjectMapper();
String ip = request.getRemoteAddr();
// 执行函数前打印日志
// mapper.writeValueAsString(paramArgs) 会读取response,所以当需要在逻辑里面使用response的时候(比如导出)需要注释掉这个
logger.info("调用前:{}: {},传递的参数为:{}",className,methodName,mapper.writeValueAsString(paramArgs));
//logger.info("调用前:{}: {}",className,methodName);
logger.info("URL:{}",request.getRequestURL().toString());
logger.info("IP地址:{}", ip);
Object proceed = joinPoint.proceed();// 放行
logger.info("成功调用,耗时:{}ms", System.currentTimeMillis() - threadLocal.get());
PcLog pcLog = new PcLog();
this.insertPcLog(methodName, paramArgs, ip, pcLog);
return proceed;
}
@AfterThrowing(pointcut = "performance()" , throwing = "ex")
public void ErrorLog(JoinPoint joinPoint, Exception ex) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
System.out.println("========进入异常=======");
String methodName = joinPoint.getSignature().getName();
Object[] paramArgs = joinPoint.getArgs();
ObjectMapper mapper = new ObjectMapper();
String ip = request.getRemoteAddr();
PcLog pcLog = new PcLog();
if (ex instanceof PlanException) {
Integer type = ((PlanException) ex).getType();
pcLog.setLogCode(type);
EamErrorType eamErrorType = EamErrorType.fromValue(type);
pcLog.setCodeName(eamErrorType.name());
} else {
pcLog.setLogCode(-1);
pcLog.setCodeName(ex.getClass().getName());
}
try {
if (ex.getMessage() == null) {
pcLog.setErrorMessage(ex.getClass().getName());
} else {
pcLog.setErrorMessage(ex.getMessage().length() > 500?ex.getMessage().substring(0,500): ex.getMessage());
}
StackTraceElement[] stackTrace = ex.getStackTrace();
StringBuilder sb = new StringBuilder();
for (StackTraceElement element : stackTrace) {
sb.append(element.toString());
sb.append("\n");
}
pcLog.setTrace(sb.toString());
} catch (Exception e) {
String errMsg = MyPlanConstant.LOGINFO + MyPlanConstant.DATAPK;
logger.error(errMsg);
throw new PlanException(EamErrorType.PC_SQL_OPERATE_ERROR.getValue(), errMsg);
}
this.insertPcLog(methodName, paramArgs, ip, pcLog);
System.out.println();
}
public void insertPcLog(String methodName, Object[] paramArgs, String ip, PcLog pcLog) {
System.out.println("=======存储错误日志=====");
try {
Long userId = PermissionAspect.getUserId();
pcLog.setAccessorId(userId); // 调用者
pcLog.setAccessorName(myCommonUtil.getUserById(userId.toString()).getString("nickName"));
} catch (Exception e) {
logger.error(e.getMessage() , e);
}
try {
pcLog.setAccessorType("USER");
pcLog.setPortName(methodName);
pcLog.setIp(ip);
pcLog.setCreateTime(new Date());
pcLog.setProps(Arrays.toString(paramArgs));
logMapper.insert(pcLog);
} catch (Exception e) {
String errMsg = MyPlanConstant.PC_LOG_INSERT_ERROR;
logger.error(errMsg);
throw new PlanException(EamErrorType.PC_LOG_INSERT_ERROR.getValue(), errMsg);
}
}
}
注意
1.此时的ip获取可能会有问题,可以参考这篇文章进行修改
2.接口的参数是,实体类的时候需要实现toString方法,要不然参数会存储对象地址而不是对象的值
5.2 参数不为空的判断
- 先定义一个注解
- 切入点为注解的切面
package net.facelib.eam.plancenter.webController.config.param;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 参数注解
*
* @author lc
* @version 1.0
* @date 2022/8/18 16:19
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequestRequire {
/**
* 请求当前接口所需要的参数,多个以小写的逗号隔开
* @return
*/
public String require() default "";
/**
*传递参数的对象类型
*/
public Class parameter() default Object.class;
}
package net.facelib.eam.plancenter.webController.config.param;
import cn.hutool.core.util.ObjectUtil;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
* 参数校验切面
*
* @author lc
* @version 1.0
* @date 2022/8/18 16:16
*/
@Aspect
@Component
public class ParamAspect {
@Around("@annotation(net.facelib.eam.plancenter.webController.config.param.RequestRequire)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
//获取注解的方法参数列表
Object[] args = pjp.getArgs();
//获取被注解的方法
MethodInvocationProceedingJoinPoint mjp = (MethodInvocationProceedingJoinPoint) pjp;
MethodSignature signature = (MethodSignature) mjp.getSignature();
Method method = signature.getMethod();
//获取方法上的注解
RequestRequire require = method.getAnnotation(RequestRequire.class);
//以防万一,将中文的逗号替换成英文的逗号
String fieldNames = require.require().replace(",", ",");
//从参数列表中获取参数对象
Object parameter = null;
for (Object pa : args) {
//class相等表示是同一个对象
if (pa.getClass() == require.parameter()) {
parameter = pa;
}
}
//通过反射去和指定的属性值判断是否非空
Class cl = parameter.getClass();
for (String fieldName : fieldNames.split(",")) {
//根据属性名获取属性对象
Field field = cl.getDeclaredField(fieldName);
//设置可读写权限
field.setAccessible(true);
//获取参数值,因为我的参数都是String型所以直接强转
Object value = field.get(parameter);
//非空判断
if (!ObjectUtil.isNotEmpty(value)) {
throw new RuntimeException("参数:" + fieldName + "不允许为空");
}
}
//如果没有报错,放行
return pjp.proceed();
}
}
/**
* 测试
*/
@GetMapping("/test")
@ApiOperation(value = "test")
@RequestRequire(require = "applyName,connectId", parameter = ApplyDto.class)
public Response test(ApplyDto applyDto) {
Response response = responseFactory.createResponse();
response.onComplete("just test");
return response;
}
六、注意事项
6.1 切面的执行顺序问题
spring的很多功能的实现都是切面aop实现的,所以,假如自己要定义一个切面的时候,可能最好设置执行顺序;
比如,我们自定义一个@AfterThrowing异常通知来存储存储错误日志,假如我们不定义顺序的话,可能没有插入数据表中,插入日志的代码也没报任何错误;
因为,当我们自己写aop拦截的时候,会遇到跟spring的事务aop执行的先后顺序问题,比如说动态切换数据源的问题,如果事务在前,数据源切换在后,会导致数据源切换失效,所以就用到了Order(排序)这个关键字.我们可以通过在@AspectJ的方法中实现org.springframework.core.Ordered 这个接口来定义order的顺序,order 的值越小,说明越先被执行。
@Order 可以作用作用在类上
注解@Order或者接口Ordered的作用是定义Spring IOC容器中Bean的执行顺序的优先级,而不是定义Bean的加载顺序,Bean的加载顺序不受@Order或Ordered接口的影响