一、前言
在项目开发过程中,经常会遇到需要对操作日志进行存储,如果在每个接口中都调用日志服务,会增加耦合度。此时我们可以使用AOP + 注解的方式来实现。
二、定义注解
首先要定义一个注解,我们可以在注解中定义一些属性,也可以设置一些默认值。
package aop.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/*
* Documented
* 指示默认情况下带有类型的注释将由javadoc和类似的工具记录。
* 此类型应用于注释其注释影响其客户端使用带注释元素的类型的声明。
* 如果类型声明使用Documented注释,则其注释将成为注释元素的公共API的一部分。
*/
@Documented
/*
* Retention
* 指示带批注类型的批注要保留多长时间
* 如果批注类型声明上不存在保留批注,则保留策略默认为 RetentionPolicy.CLASS。
* 只有当元注释类型直接用于注释时,保留元注释才有效。如果元注释类型用作另一个注释类型中的成员类型,则它没有效果
*/
@Retention(RetentionPolicy.RUNTIME)
/*
* Target
* 指示批注类型适用的上下文,也就是使用注解的目标
*/
@Target(ElementType.METHOD)
public @interface Log {
/**
* 操作类型
*/
String type() default "";
/**
* 操作人
*/
String user() default "admin";
}
三、编写切面
1、@Aspect 注解表示这是一个切面类
2、@Pointcut 表示切入点,也就是说在什么节点切入,定义切点的切入范围。
3、@Around 表示环绕通知。通知类型还包括前置通知(@Before)、后置通知(@After)、异常通知(@AfterThrowing)、最终通知(AfterReturning)。
我们优先使用环绕通知,可以自定义实现切点的实现逻辑。其他通知的联合使用可能会因为指令重排导致顺序错误。
package aop.annotation;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* @Description 切面
* @Author King
* @Date 2019/11/15 16:32
* @Version 1.0
**/
@Aspect
@Component
public class LogAspect {
/**
* 定义切入点的范围
* annotation 标识针对注解切入
*/
@Pointcut("@annotation(aop.annotation.Log)")
private void logAspect() {
}
/**
* 环绕通知
*/
@Around("logAspect()")
public Object around(ProceedingJoinPoint point) throws Throwable {
// 连接点参数
Object[] args = point.getArgs();
MethodSignature methodSignature = (MethodSignature) point.getSignature();
// 获取注解
Log annotation = methodSignature.getMethod().getAnnotation(Log.class);
System.out.printf("【日志注解】---请求参数: %s,操作类型: %s,操作人: %s %n", Arrays.toString(args),
annotation.type(), annotation.user());
return point.proceed();
}
}
四、业务接口
1、定义一个业务接口,两个方法,一个用注解,一个不用注解,用以区分使用注解进入切面。
package aop.annotation;
public interface UserService {
/**
* 保存用户
* @param name 用户名
* @param work 工作
*/
void save(String name, String work);
/**
* 删除用户
* @param name 用户名
*/
void delete(String name);
}
package aop.annotation;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Override
@Log(type = "新增", user = "root")
public void save(String name, String work) {
// 处理业务逻辑
System.out.printf("【业务逻辑】---添加用户,姓名:%s,工作:%s %n", name, work);
}
@Override
public void delete(String name) {
// 处理业务逻辑
System.out.printf("【业务逻辑】---删除用户,姓名:%s %n", name);
}
}
2、定义一个不实现接口的类。用以区别不同代理,AOP是基于代理实现的。有JDK代理和CGLB代理。
package aop.annotation;
import org.springframework.stereotype.Service;
@Service
public class CGLBService {
@Log(type = "打招呼")
public void hello(String name) {
System.out.println("【业务逻辑】---你好:" + name);
}
}
五、测试
1、我们需要在启动类上添加@EnableAspectJAutoProxy注解。该注解表示能够发现标有@Aspect注解的类。功能类似于在xml中配置 <aop:aspectj-autoproxy>
2、@ComponentScan 注解表示我们要扫描的包。只要是标有@Component注解的类都会被加载到Spring容器中
package aop;
import aop.annotation.CGLBService;
import aop.annotation.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* @Description AOP实现日志注解
* EnableAspectJAutoProxy注解的作用:
* 启用处理标有AspectJ的组件支持@Aspect注解,类似的功能在Spring的发现<aop:aspectj-autoproxy> XML元素
* @Author King
* @Date 2020/4/1 14:25
* @Version 1.0
**/
@EnableAspectJAutoProxy
@ComponentScan("aop.annotation")
public class LogAspectTest {
/**
* Spring AOP 实现流程
* <p>
* 1、根据切点(Pointcut)获取目标类、目标方法
* <p>
* 2、根据增强(Advice)(5种通知)嵌入增强逻辑
* <p>
* 3、通过切面(Advisor)(@Aspect注解)把切点与增强装配起来
* <p>
* 4、使用 JDK或CGLib动态代理技术为目标类创建已织入切面的代理对象
*/
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(LogAspectTest.class);
// 面向接口 默认使用jdk动态代理 EnableAspectJAutoProxy属性proxyTargetClass设置为true使用CGLB代理
UserService userService = context.getBean(UserService.class);
// 使用注解 会进行增强
userService.save("巴扎黑", "学习Java");
// 未使用注解 不会进行增强
userService.delete("巴扎黑");
System.out.println("==============================================================");
// 未实现接口 使用CGLB代理 不能是final类(实现原理:CGLB生成子类继承被代理对象)
CGLBService cglbService = context.getBean(CGLBService.class);
cglbService.hello("巴扎黑");
}
}
六、测试结果
Spring AOP 实现流程
1、根据切点(Pointcut)获取目标类、目标方法
2、根据增强(Advice)(5种通知)嵌入增强逻辑
3、通过切面(Advisor)(@Aspect注解)把切点与增强装配起来
4、使用 JDK或CGLib动态代理技术为目标类创建已织入切面的代理对象