1.AOP介绍
AOP:Aspect-Oriented Programming的缩写
JoinPoint:要切入的点,本文章的注解@OperLog就是要切入的点。
Pointcut:定系统中符合条件的一组Joinpoint。
Aspect:就是我们的切面,例如我们的日志类,里边包含了记录操作日志,记录异常日志等,那么这个对象就是一个切面。
Advice:怎么切,是在业务逻辑执行前去执行还是之后执行等。
1.1什么是AOP
AOP(Aspect-OrientedProgramming,面向方面编程),可以说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善。OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
而AOP技术则恰恰相反,它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。AOP代表的是一个横向的关系,如果说“对象”是一个空心的圆柱体,其中封装的是对象的属性和行为;那么面向方面编程的方法,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息。而剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。
使用“横切”技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处都基本相似。比如权限认证、日志、事务处理。Aop 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。正如Avanade公司的高级方案构架师Adam Magee所说,AOP的核心思想就是“将应用程序中的商业逻辑同对其提供支持的通用服务进行分离。”
实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。
1.2 AOP使用场景
AOP用来封装横切关注点,具体可以在下面的场景中使用:
Authentication 权限
Caching 缓存
Context passing 内容传递
Error handling 错误处理
Lazy loading 懒加载
Debugging 调试
logging, tracing, profiling and monitoring 记录跟踪 优化 校准
Performance optimization 性能优化
Persistence 持久化
Resource pooling 资源池
Synchronization 同步
Transactions 事务
1.3 AOP相关概念
一些共同操作的抽取,或者说是一个关注点的模块化,这个关注点可能会横切多个对象。
比如上述代码中数据库的事务直接贯穿了整个代码层面,这就是一个切面,它能够在被代理对象的方法执行之前、之后、 产生异常或者正常返回后切入你的代码,甚至代替原来被代理对象的方法,在动态代理中可以把它理解成一个拦截器。
在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在 Spring AOP 中,一个连接点就表示一个方法的执行。用大白话说就是在哪个方法执行的时候需要开始拦截,以执行自己定义的增强逻辑,这个方法就是一个连接点。
如果说连接点是具体到某个方法执行,那么切点就可以理解为某类方法的集合,它定义的是一个范围。因为并不是所有的开发都需要启动 AOP,所以通常使用正则表达式进行限定,来指定某类方法执行时启动 AOP 功能。
通知是切面开启后,切面中的方法。它根据在代理对象真实方法调用前、后的顺序和逻辑区分。
- 前置通知(before):在动态代理反射原有对象方法或者执行环绕通知前执行的通知功能。
- 后置通知(after):在动态代理反射原有对象方法或者执行环绕通知后执行的通知功能。无论是否抛出异常,它都会被执行。
- 返回通知(afterReturning):在动态代理反射原有对象方法或者执行环绕通知后执行的通知功能。
- 异常通知(afterThrowing ):在动态代理反射原有对象方法或者执行环绕通知产生异常后执行的通知功能。
- 环绕通知(aroundThrowing):在动态代理中,它可以取代当前被拦截对象的方法,提供回调原有被拦截对象的方法。
织入其实是一个过程:当程序执行到连接点时,就会生成代理对象并将切面内容放入到流程当中。
2. 实践
2.1 编写注解OperLog
import java.lang.annotation.*;
/**
* @Auther: liuysh
* @Date: 2021/4/24 11:07
* @Description:
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperLog {
String value() default "";
}
2.2 编写切面LogOperationAspect
package com.sugon.cloud.aop;
import com.google.common.base.Throwables;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Date;
/**
* @Auther: liuysh
* @Date: 2021/4/24 11:06
* @Description:
*/
@Aspect
@Component
public class LogOperationAspect {
private static final Logger logger = LogManager.getLogger("LogConfig");
@Pointcut("@annotation(com.sugon.cloud.aop.OperLog)")
public void point() { }
@Around(value = "point()")
public Object pointAround(ProceedingJoinPoint joinPoint) throws Throwable{
long beginTime = System.currentTimeMillis();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
OperLog operLog=method.getAnnotation(OperLog.class);
String class_name = joinPoint.getTarget().getClass().getName();
String method_name = method.getName();
// 请求的参数
Object[] method_args = joinPoint.getArgs();
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
String ip=getIpAddr(requestAttributes.getRequest());
String requestURI=requestAttributes.getRequest().getRequestURI();
Object r= joinPoint.proceed(method_args);
long endTime = System.currentTimeMillis();
logger.info("用户操作:{} ,requestURI:{},请求类:{},请求函数:{},请求参数:{},返回结果:{}",
operLog.value(),requestURI,class_name,method_name, Arrays.toString(method_args),r.toString());
logger.info("请求ip:{},结束时间: {},耗时:{}ms",
ip, new Date(),endTime - beginTime);
return r;
}
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
logger.error("获取ip异常:{}" , Throwables.getStackTraceAsString(e));
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress = "";
}
return ipAddress;
}
}
2.3 使用方法
@OperLog("注解在controller 方法")
2.4 日志输出结果