前言

AOP(面向切面编程)作为Spring框架的两大重要特征之一,无论在日常工作还是面试中出现的频率都很高,下面从作者日常工作中的应用和学习来详细解析一下AOP。


一、AOP是什么?

面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术,常用功能为日志记录,性能统计,安全控制,事务处理,异常处理(内容来自百度百科),通过上面我们可以了解到,AOP主要是将一些公共方法从业务方法中剥离出来,做成统一的代码块。

二、AOP原理

AOP底层实现采用的是动态代理,分为两种情况,对于有接口的采用JDK动态代理,对于没有接口的采用CGLIB动态代理。JDK动态代理只提供接口的代理,不支持类的代理
JDK运行时为目标类生成一个动态代理类$proxy*.class,该代理类实现了目标类接口,并且实现接口所有的方法增强代码,调用时,先增强,然后通过反射来调用目标方法;CGLIB代理底层是通过ASM在运行时动态的生成一个目标类的子类,通过继承的方式做的动态代理,因此若某个类被标记为final,那么他是无法使用CGLIB做动态代理

创建UserService接口实现类UserServiceImpl的代理对象,增强类的方法
使用Proxy类的newProxyInstance方法动态代理,三个参数:类加载器、增强方法所在的类,这个类实现的接口,支持多个接口、实现这个接口Invocationhandler,创建代理对象,写增强方法

public class JDKProxy {
    public static void main(String[] args) {
        //创建接口实现类代理对象
        Class[] interfaces = {UserService.class};
//        Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, new InvocationHandler() {
//            @Override
//            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//                return null;
//            }
//        });
        UserServiceImpl userServiceImpl = new UserServiceImpl();
        UserService userService = (UserService) Proxy.newProxyInstance(JDKProxy.class.getClassLoader(),interfaces,new UserServiceProxy(userServiceImpl));
        int sum = userService.add(4,6);
        System.out.println(sum);
    }
}


//创建代理对象
class UserServiceProxy implements InvocationHandler{


    /**
     * 创建谁的代理对象,要把谁传递进来
     * 有参函数构造传递
     */
    private Object obj;
    public UserServiceProxy(Object obj){
        this.obj = obj;
    }


    //增强的逻辑
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //方法之前
        System.out.println("方法之前执行"+method.getName()+"传递的参数"+ Arrays.toString(args));
        //被增强方法执行
        Object res = method.invoke(obj,args);
        //方法之后
        System.out.println("方法之后执行"+obj);
        return res;
    }
}

public interface UserService {
    public int add(int a,int b);
    public String update(String id);
}

public class UserServiceImpl implements UserService {
    @Override
    public int add(int a, int b) {
        System.out.println("方法执行了");
        return a+b;
    }


    @Override
    public String update(String id) {
        return id;
    }
}

结果
方法之前执行add传递的参数[4, 6]
方法执行了
方法之后执行com.zy.bysj.AOP.UserServiceImpl@36aa7bc2
10
class User{
    public void add(){
        ......
    }
}

class Student extends User{
    public void addStudent(){
        super.add();
        //增强逻辑
    }
}
创建当前类的自类的代理对象,参考上面JDK动态代理代码

三、AOP使用

1.几个术语定义

切面aspect:指定就是切面类,切面类会管理着切点、通知
通知advice:需要增强到业务方法中的公共代码,在需要增加的业务方法不同位置执行(前置通知、后置通知、异常通知、环绕通知)
切点pointcut:决定哪些方法需要增强,哪些不需要增强,结合切点表达式实现
连接点join point:被增强的业务方法
目标对象:增强的对象

2.切点和通知

下面通过一个实例代码重点解析一下通知和切点

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.zy.bysj.utils.IpUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
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 javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;


/**
* 使用场景:生成日志、管理权限、委托事务等
* 定义拦截类:指定切入点、切面,并做拦截处理
*/
@Component//指明这是一个组件,被spring接管
@Aspect//指明这是一个拦截器
@Order(1)//当存在多个切面时,该注解指定执行的优先级:参数越小,优先级越高,主要是在一个接口有多个切面校验时使用
public class LogTrackAspect {
    private static final Logger log = LoggerFactory.getLogger(LogTrackAspect.class);
    /**
     * 定义切点方法:可获取注解标注处配置参数,也可以获得目标方法处注入参数(argName可以省略)
     * 保证切点方法参数列表与@Point注解value值保持一致
     * 定义一个切点,所有@LogTrack注解修饰的会被织入advice
     * 四个常用的表达式:execution(),annotation(),@within(),@target()这里主要使用第一个,其他的使用如下所示
     * 以execution(* * com.zy.bysj.controller..*.*(..)))为例:
     * 第一个*号的位置,表示返回值类型,*代表所有类型
     * 第二个*号的位置,表示类名,*表示所有类
     * 包名:表示需要拦截的包名,后面两个点表示当前包和当前包的所有子包,表示在com.zy.bysj.controller包,子包下的所有类的方法
     * *(..):这个星号表示所有的方法,后面括号里表示方法的参数,两个句点表示任何参数
     *@Pointcut("@within(org.apache.dubbo.config.annotation.Service)")匹配所有持有指定注解的类里面*的方法, 即要把注解加在类上. 在接口上声明不起作用
     *@target(注解类型全限定名)匹配当前目标对象类型的执行方法, 必须是在目标对象上声明注解,在接*口上声明不起作用
     */
    @Pointcut(value = "@annotation(com.zy.bysj.AOP.LogTrack)")
    public void access(){
    }


    /**
     * 定义一个切点,拦截com.zy.bysj.controller.AopController包下的所有方法
     */
    @Pointcut("execution(* com.zy.bysj.controller.AopController..*.*(..))")
    public void pointCut(){
    }
    //进入切点,先经过的第一站
    //before表示doBefore方法将在目标方法执行前执行
    //@Before 注解指定的方法在切面切入目标方法之前执行,可以做一些 Log 处理,也可以做一些信息的统计,比如获取用户的请求 URL 以及用户的 IP 地址等等
    @Before("pointCut()")
//    @Before("access()")
    public void doBefore(JoinPoint joinPoint){
        log.info("-aop日志记录启动-doBefore方法进入"+new Date());
        //获取签名
        Signature signature = joinPoint.getSignature();
        //获取切入的包名
        String declaringTypeName = signature.getDeclaringTypeName();
        //获取即将执行的方法名
        String funcName = signature.getName();
        log.info("即将执行的方法为:{},属于{}包",funcName,declaringTypeName);
        //也可以记录一些信息,比如获取请求的URL和IP
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String URL = request.getRequestURI();
        String IP = request.getRemoteAddr();
        log.info("请求的url为:{},ip地址为{}",URL,IP);
    }


    /**
     * 环绕增强,在before前就会触发
     * @param pjp
     * @param logTrack
     * @return
     * @throws Throwable
     * 两个特性:
     * 1、@Around注解可以自由选择增强动作与目标方法的执行顺序,也就是可以在增强动作前后,甚至过程中执行目标方法,
     * 这个特性的实现在于,调用ProceedingJoinPoint参数的proceed()方法才会执行目标方法
     * 2、@Around可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值
     * 当定义一个Around增强处理方法时,该方法的第一个形参必须是ProceedingJoinPoint类型,调用其proceed方法才会执行目标方法
     * 调用proceed方法时,还可以传入一个Object[]对象,该数组中的值将被传入目标方法作为实参,这是around增强处理方法可以改变目标方法参数值的关键
     * @注意:功能虽然强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的Before、AfterReturning就能解决的问题,就没有必要使用Around了。如果需要目标方法执行之前和之后共享某种状态数据,则应该考虑使用Around。尤其是需要使用增强处理阻止目标的执行,或需要改变目标方法的返回值时,则只能使用Around增强处理了。
     */
//    @Around("access()")直接写切点切入的方法也可
    @Around("@annotation(logTrack)")
    public Object around(ProceedingJoinPoint pjp,LogTrack logTrack) throws Throwable {
        System.out.println("-aop 日志环绕阶段-"+new Date());
        //根据ServletRequestAttributes 获取前端请求方法名、参数、路径等信息
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String url = request.getRequestURI();
        String ip = IpUtils.getIpAddr(request);
        /**
         * 下面三个参数对应的自定义注解的三个变量,获取到的是在controller层定义的值
         */
        String logTrackValue = logTrack.value();
        int num = logTrack.num();
        String opertion = logTrack.opertion();
        Object[] pipArray = pjp.getArgs();
        //多参,不是MAP/JsonObject方式
        if(pipArray.length>1){
            List<Object> argList = new ArrayList<>();
            for(Object arg:pjp.getArgs()){//这里arg对应的是参数的值
                //request/response无法使用toJSON
                if(arg instanceof HttpServletRequest){//instanceof关键字,严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例,是则返回true
                    argList.add("request");
                }else if(arg instanceof HttpServletResponse){
                    argList.add("response");
                }else{
                    argList.add(JSON.toJSON(arg));
                }
            }
            //接口签名
            Signature signature = pjp.getSignature();
            MethodSignature methodSignature = (MethodSignature) signature;
            //参数名数组
            String[] parameterNames = ((MethodSignature) signature).getParameterNames();
            System.out.println("参数名数组:"+new ArrayList(Arrays.asList(parameterNames)));
            System.out.println("参数是:"+argList.toString());
            System.out.println("模块名称:"+logTrackValue);
            System.out.println("操作编号:"+num);
            System.out.println("操作名称:"+opertion);
            System.out.println("url:"+url);
            System.out.println("ip:"+ip);
            return pjp.proceed();
        }else{
            Object param = pipArray[0];
            System.out.println("logTrackValue:"+logTrackValue);
            System.out.println("url:"+url);
            System.out.println("ip:"+ip);
            //这里打印的是传入的参数
            System.out.println("param:"+param.toString());
            /**
             * 这里尝试修改参数,目标方法里打印的会是修改的参数
             * 如果传入的Object[ ]数组长度与目标方法所需要的参数个数不相等,或者Object[ ]数组元素与目标方法所需参数的类型不匹配,程序就会出现异常
             */
            JSONObject object = new JSONObject();
            object.put("name","zhangsan");
            object.put("age",10);
            pipArray[0] = object;
            return pjp.proceed(pipArray);
        }
    }
    //return pjp.proceed(); 这个是从切点的环绕增强里面脱离出来,接下来会进入before阶段 ,然后回到接口,再回来after阶段。
    //所以扩展业务逻辑处理的话,可以放在return pjp.proceed();这行代码之前,例如判断用户密码是否正确;判断用户权限等等


    //进来切点,最后经过的一站,也是方法正常结束后
    //@After 注解和 @Before 注解相对应,指定的方法在切面切入目标方法之后执行,也可以做一些完成某方法之后的 Log 处理。
    @After("pointCut()")
//    @After("access()")
    public void after(JoinPoint joinPoint){
        log.info("-aop-拦截开始-"+new Date());
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        log.info("方法{}已经执行完成",method);
    }


    /**
     * 在上面定义的切面方法返回后执行该方法,可以捕获返回对象或者对返回对象进行增强
     * @param joinPoint
     * @param object
     */
    @AfterReturning(pointcut = "pointCut()",returning = "object")
    public void doAfterReturning(JoinPoint joinPoint,Object object){
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        log.info("方法{}执行完成,返回参数为{}",method,object);
        //实际项目中可以根据业务做具体的返回值增强
        log.info("对返回的参数进行业务上的增强:{}",object+"增强版");
    }


    /**
     * 当被切方法执行过程中抛出异常时,会进入 @AfterThrowing 注解的方法中执行,在该方法中可以做一些异常的处理逻辑。
     * 要注意的是 throwing 属性的值必须要和参数一致,否则会报错。该方法中的第二个入参即为抛出的异常。
     * @param joinPoint
     * @param ex
     */
    @AfterThrowing(pointcut = "access()",throwing = "ex")
    public void doAfterThrowing(JoinPoint joinPoint,Throwable ex){
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        //处理异常的逻辑
        log.info("执行方法{}出错,异常为{}",method,ex);
    }
}

下面也是一个AOP切面的结构

public class ControllerAop {
    @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
    public void postAspect() {

    }
    @Around("postAspect()")
    public Object beforePostAspect(ProceedingJoinPoint joinPoint) throws Throwable {
        return doSomething(joinPoint);
    }

    public Object doSomething(ProceedingJoinPoint joinPoint) throws Throwable {
        Class<?> clazz = joinPoint.getTarget().getClass();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        for (int i = 0; i < parameterAnnotations.length; i++) {
            Annotation[] annotations = parameterAnnotations[i];
            for (Annotation annotation : annotations) {
         
                } 
        }
        Object result = joinPoint.proceed(args);
    }
    @AfterThrowing(pointcut = "postAspect()", throwing = "e")
    public void handlePostError(JoinPoint joinPoint, Exception e) throws Exception {
        sendErrorLog(joinPoint, e);
        throw e;
    }
    private void sendErrorLog(JoinPoint joinPoint, Exception e) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    }
}

总结

AOP在日常项目中的使用还是很频繁的,尤其是记录系统日志使用更为频繁,因此本文记录了一下在项目中如何使用AOP,并且浅析了AOP的一些相关定义和概念。