前言
作为java开发工程师,日常curd工作少不了,特别是后台系统的操作,对于每一项操作我们都要记录,所以就得有操作日志,操作日志能够排除是开发的锅,是运营或者产品自己操作的。那么就有个问题,每次在业务处理最后,调用操作日志服务保存响应的日志,但是这段代码是很冗余的。
其实想要解决这个问题,方法很多,可以用工厂模式,日志模板,日志切面等等。本讲就是利用Spring的AOP面向切面编程来实现日志操作,Sping官网也说了AOP主要用于日志,事务等处理,同时,自定义一个日志注解,那些业务操作需要记录日志,只要添加对应的注解就行,使得业务代码和日志代码低耦合。
一、AOP面向切面编程
AOP(Aspect Oriented Programming,面向切面编程),可以说是 OOP(Object Oriented Programing,面向对象编程)的补充和完善。OOP 引入封装、继承和多态性等概念来建立一种对象层次结构,用来模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP 允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能,日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如权限管理、异常处理等也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在 OOP 设计中,它导致了大量代码的重复,而不利于各个模块的重用。
而 AOP 技术则恰恰相反,它利用一种称为 “横切” 的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为 “Aspect” ,即切面。**所谓“切面”,简单地说,就是将权限、事务、日志、异常等与业务逻辑相对独立的功能抽取封装,便于减少系统的重复代码,降低模块间的耦合度,增加代码的可维护性。**AOP 代表的是一个横向的关系,如果说 “对象” 是一个空心的圆柱体,其中封装的是对象的属性和行为;那么面向切面编程,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息,然后又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。
所以,AOP可以使用如下场景:
- 权限控制
- 日志存储
- 统一异常处理
- 缓存处理
- 事务处理
1.常用注解
AOP底层是用动态代理实现,依托的是采用的是JDK/CGLIB动态代理。不理解代理可以这一篇文章:代理模式
AOP常用注解:
@Pointcut :切入点,可以指定那个包/那个类,进行扫描
@Before :前置通知,在目标方法调用前执行通知
@After :后置通知,在目标方法完成(不管是抛出异常还是执行成功)后执行通知
@Around :环绕通知,在目标方法调用前后均可执行自定义逻辑
@AfterThrowing :异常通知,出现异常被执行
@AfterReturning:返回通知,在目标方法执行成功后,调用通知
2.AOP原理
定义AOP切面Aspect,在spring容器初始化的时候,底层会通过动态代理的方式生成代理对象,当程序调用方法是在切入点内Pointcut,会调用到生成的字节码文件中,直接会找到DynamicAdvisoredIntercetor类的intercept方法,根据定好的通知器(Before/After/Around)会先生成一个拦截器链,依次执行,这一例操作都是异步的。
二、SpringBoot整合AOP
废话不多说,接下来我们通过代码案例,整合AOP,本次主要依托springboot工程,
导入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
创建切面类:@Aspect标记为切面,开启AOP
@Component
@Aspect
public class LogAspect {
}
实现对应的通知器:
切入点:* com.qm.springbootlogaspect.service.impl…*(…)) 即扫描service.impl这个包的所有类
@Component
@Aspect
public class LogAspect {
private Logger logger = LoggerFactory.getLogger(LogAspect.class);
/**
* 配置aop切点,即扫描impl包下面的类和包是否加log注解
*/
@Pointcut("execution(* com.qm.springbootlogaspect.service.impl..*(..))")
private void pointcut() {
}
@Before(value = "pointcut()")
public void saveUserOperate(JoinPoint joinPoint)throws NotFoundException, ClassNotFoundException {
System.out.println("开始新增操作日志------前置操作");
}
@After(value = "pointcut()")
public void After(JoinPoint joinPoint) throws NotFoundException, ClassNotFoundException {
System.out.println("新增操作日志结束"+user.getId()+"保存数据库------后置操作");
}
测试 :启动springboot工程,控制器调用UserServiceImpl,可以发现输出上面前置后置通知器的内容。
这样我们就可以对所有业务操作之后,自动进行日志操作保存了
三、自定义日志注解
上面通过整合AOP,可以已经实现了日志操作采集,但是是有没有发现。在切入点扫描下的所有包,都会记录日志,虽然控制了只扫描某个包,但是如果业务类多的话,对程序性能会有一定的影响。另一个问题是,我们没发知道是谁操作了,aop的通知器无法获取到操作用户的信息,也是业务类的信息。
基于这个问题,我们想到可以注解作为载体,在切面点扫描的包,只有业务方法标记了自定义注解,才会保存操作日志,同时可以把业务信息(用户信息)通过注解传递给切面。
1.自定义日志注解
Java自定义注解很简单,注解中有个属性logStr,这个可以日志信息传递到切面。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Inherited
@Documented
public @interface Log {
String logStr() default "";
}
2.定义注解解析类
新建日志工具类,用于日志保存的调用
public class LogUtils {
private static LogUtils mInstance;
private LogUtils() {
}
public static LogUtils get() {
if (mInstance == null) {
synchronized (LogUtils.class) {
if (mInstance == null) {
mInstance = new LogUtils();
}
}
}
return mInstance;
}
public void setLog(String logStr) throws NotFoundException {
String className = Thread.currentThread().getStackTrace()[2].getClassName();
String methodName = Thread.currentThread().getStackTrace()[2].getMethodName();
AnnotationUtils.get().setAnnotatioinFieldValue(className, methodName, Log.class.getName(), "logStr", logStr);
}
}
新建注解内容解析类AnnotationUtils,主要可以修改注解上的属性值,获取注解中的属性值
public class AnnotationUtils {
private static AnnotationUtils mInstance;
public AnnotationUtils() {
}
public static AnnotationUtils get() {
if (mInstance == null) {
synchronized (AnnotationUtils.class) {
if (mInstance == null) {
mInstance = new AnnotationUtils();
}
}
}
return mInstance;
}
/**
* 修改注解上的属性值
*
* @param className 当前类名
* @param methodName 当前方法名
* @param annoName 方法上的注解名
* @param fieldName 注解中的属性名
* @param fieldValue 注解中的属性值
* @throws NotFoundException
*/
public void setAnnotatioinFieldValue(String className, String methodName, String annoName, String fieldName, String fieldValue) throws NotFoundException {
ClassPool classPool = ClassPool.getDefault();
CtClass ct = classPool.get(className);
CtMethod ctMethod = ct.getDeclaredMethod(methodName);
MethodInfo methodInfo = ctMethod.getMethodInfo();
ConstPool constPool = methodInfo.getConstPool();
AnnotationsAttribute attr = (AnnotationsAttribute) methodInfo.getAttribute(AnnotationsAttribute.visibleTag);
Annotation annotation = attr.getAnnotation(annoName);
if (annotation != null) {
annotation.addMemberValue(fieldName, new StringMemberValue(fieldValue, constPool));
attr.setAnnotation(annotation);
methodInfo.addAttribute(attr);
}
}
/**
* 获取注解中的属性值
*
* @param className 当前类名
* @param methodName 当前方法名
* @param annoName 方法上的注解名
* @param fieldName 注解中的属性名
* @return
* @throws NotFoundException
*/
public String getAnnotatioinFieldValue(String className, String methodName, String annoName, String fieldName) throws NotFoundException {
ClassPool classPool = ClassPool.getDefault();
CtClass ct = classPool.get(className);
CtMethod ctMethod = ct.getDeclaredMethod(methodName);
MethodInfo methodInfo = ctMethod.getMethodInfo();
AnnotationsAttribute attr = (AnnotationsAttribute) methodInfo.getAttribute(AnnotationsAttribute.visibleTag);
String value = "";
if (attr != null) {
Annotation an = attr.getAnnotation(annoName);
if (an != null)
value = ((StringMemberValue) an.getMemberValue(fieldName)).getValue();
}
return value;
}
}
3.测试
在使用中,我们只要在需要日志保存的业务标记,@Log注解,如果需要保存对于业务信息,可以用LogUtils,把业务信息放入Log注解的字段中。如UserServiceImpl类:
@Log
public String findUserName(String tel) {
System.out.println("tel:" + tel);
User user = new User();
user.setId("1").setName("aaa").setCreateTime(new Date());
try {
LogUtils.get().setLog(JSON.toJSONString(user));
} catch (NotFoundException e) {
e.printStackTrace();
}
return "zhangsan";
}
切面后置处理器,增加日志注解判断,只有加了 @Log注解才会进行保存操作
@After(value = "pointcut()")
public void After(JoinPoint joinPoint) throws NotFoundException, ClassNotFoundException {
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
String logStr = AnnotationUtils.get().getAnnotatioinFieldValue(className, methodName, Log.class.getName(), "logStr");
if (!StringUtils.isEmpty(logStr)) {
// 获取json对象
JSONObject jsonObject = JSONObject.parseObject(logStr);
User user = JSON.toJavaObject(jsonObject,User.class);
logger.info("获取日志11:" + user);
// 数据库记录操作...
// 可以异步操作
System.out.println("新增操作日志结束"+user.getId()+"保存数据库------后置操作");
}
}
接口调用测试,可以看到调用接口findUserName,控制台输出对应的切面日志保存信息
总结
本文讲解什么是AOP,介绍AOP的常用注解,并且整合AOP实现日志记录,同时分析AOP记录日志的不足,加入自定义注解,更加细化精确的记录日志。本次日志记录的实现方案,也是我在项目中实际使用的,大家如果想要本文其他源码,可以订阅后私信哦。
评论区