AOP 是 Spring 体系中非常重要的两个概念之一(另外一个是 IoC),今天这篇文章实战的方式使用 AOP 技术添加一个切面来实现接口访问的统一日志记录,进行公共字段填充。
了解AOP
AOP,也就是 Aspect-oriented Programming,译为面向切面编程,是计算机科学中的一个设计思想,旨在通过切面技术为业务主体增加额外的通知(Advice),从而对声明为“切点”(Pointcut)的代码块进行统一管理和装饰。
AOP 是对面向对象编程(Object-oriented Programming,俗称 OOP)的一种补充,OOP 的核心单元是类(class),而 AOP 的核心单元是切面(Aspect)。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而降低耦合度,提高程序的可重用性,同时也提高了开发效率。
我们可以简单的把 AOP 理解为贯穿于方法之中,在方法执行前、执行时、执行后、返回值后、异常后要执行的操作。
AOP相关术语
来看下面这幅图,这是一个 AOP 的模型图,就是在某些方法执行前后执行一些通用的操作,并且这些操作不会影响程序本身的运行。
我们了解下 AOP 涉及到的 5 个关键术语:
1)横切关注点,从每个方法中抽取出来的同一类非核心业务
2)切面(Aspect),对横切关注点进行封装的类,每个关注点体现为一个通知方法;通常使用 @Aspect 注解来定义切面。
3)通知(Advice),切面必须要完成的各个具体工作,比如我们的日志切面需要记录接口调用前后的时长,就需要在调用接口前后记录时间,再取差值。通知的方式有五种:
- @Before:通知方法会在目标方法调用之前执行
- @After:通知方法会在目标方法调用后执行
- @AfterReturning:通知方法会在目标方法返回后执行
- @AfterThrowing:通知方法会在目标方法抛出异常后执行
- @Around:把整个目标方法包裹起来,在被调用前和调用之后分别执行通知方法
4)连接点(JoinPoint),通知应用的时机,比如接口方法被调用时就是日志切面的连接点。
5)切点(Pointcut),通知功能被应用的范围,比如本篇日志切面的应用范围是所有 controller 的接口。通常使用 @Pointcut 注解来定义切点表达式。
切入点表达式的语法格式规范如下所示:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)throws-pattern?)
modifiers-pattern?
为访问权限修饰符ret-type-pattern
为返回类型,通常用*
来表示任意返回类型declaring-type-pattern?
为包名name-pattern
为方法名,可以使用*
来表示所有,或者set*
来表示所有以 set 开头的类名param-pattern)
为参数类型,多个参数可以用,
隔开,各个参与也可以使用*
来表示所有类型的参数,还可以使用(..)
表示零个或者任意参数throws-pattern?
为异常类型?
表示前面的为可选项
举个例子:
@Pointcut("execution(public * com.codingmore.controller.*.*(..))")
表示 com.codingmore.controller
包下的所有 public 方法都要应用切面的通知。
在 Spring Boot 项目的 pom.xml 文件中添加 spring-boot-starter-aop 依赖。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
AOP实现公共字段填充
实现对于添加,更新数据库操作时,对于创建时间,更新时间这些公共字段代码耦合度高,使用AOP实现自动填充字段。
自定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {//数据库操作类型 INSERT UPDATEOperationType value();}
切面类:
@Aspect
@Component
@Slf4j
public class AutoFillAspect {/*** 切入点 指定自定义注解的位置*/@Pointcut("execution(* com.test.mapper.*.*(..)) && @annotation(com.test.annotation.AutoFill)")public void autoFillPointCut(){}/*** 前置通知* joinPoint 连接点对象*/@Before("autoFillPointCut()")public void autoFill(JoinPoint joinPoint){log.info("开始进行公共字段填充...");//获取当前被拦截的方法上的数据库操作 获取签名对象转型MethodSignature signature = (MethodSignature) joinPoint.getSignature();//通过反射获取注解对象AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获取操作类型OperationType operationType = autoFill.value();//获取当前被拦截的方法的参数--实体对象Object[] args = joinPoint.getArgs();if (args == null || args.length == 0){return;}//获取第一个参数Object entity = args[0];//准备赋值的数据LocalDateTime now = LocalDateTime.now();Long currentId = BaseContext.getCurrentId();//根据当前不同的操作类型,为对应的属性通过反射来赋值if (operationType == OperationType.INSERT){try {//对象的getClass方法获取class对象,获取自定义声明方法,通过反射获取类型Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);//反射赋值setCreateTime.invoke(entity , now);setCreateUser.invoke(entity , currentId);setUpdateTime.invoke(entity , now);setUpdateUser.invoke(entity , currentId);} catch (NoSuchMethodException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}} else if (operationType == OperationType.UPDATE) {try {//对象的getClass方法获取class对象,获取自定义声明方法,通过反射获取类型Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);setUpdateTime.invoke(entity , now);setUpdateUser.invoke(entity , currentId);} catch (NoSuchMethodException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}}
}
使用的时候只需要把自定义注解贴到Dao层方法上面
实现前置接口拦截,登录才能访问
自定义注解:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequireLogin {
}
拦截器:
public class RequireLoginInterceptor implements HandlerInterceptor {private StringRedisTemplate redisTemplate;public RequireLoginInterceptor(StringRedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 判断当前请求是否是一个 api 请求if (handler instanceof HandlerMethod) {HandlerMethod handlerMethod = (HandlerMethod) handler;// 从请求头中获取 Feign 请求标识,以此来判断该请求是否是 FeignString feignRequest = request.getHeader(CommonConstants.FEIGN_REQUEST_KEY);// Feign请求标识不为空 && 不是 Feign 请求 && 访问的接口方法贴了 @RequireLoginif (!StringUtils.isEmpty(feignRequest)&& CommonConstants.FEIGN_REQUEST_FALSE.equals(feignRequest)&& handlerMethod.getMethodAnnotation(RequireLogin.class) != null) {// 设置响应类型为 jsonresponse.setContentType("application/json;charset=utf-8");// 从请求头中获取 tokenString token = request.getHeader(CommonConstants.TOKEN_NAME);if (StringUtils.isEmpty(token)) {// 如果 token 为空,返回 token 无效信息response.getWriter().write(JSON.toJSONString(Result.error(CommonCodeMsg.TOKEN_INVALID)));return false;}UserInfo userInfo = JSON.parseObject(redisTemplate.opsForValue().get(CommonRedisKey.USER_TOKEN.getRealKey(token)), UserInfo.class);// 基于 token 从 redis 中获取当前用户,如果获取不到,说明 token 无效if (userInfo == null) {response.getWriter().write(JSON.toJSONString(Result.error(CommonCodeMsg.TOKEN_INVALID)));return false;}}}// 如果不是接口请求,就直接放行return true;}
}
使用直接贴在控制层接口方法上