【技术累积】【线】【java】【2】AOP

思维导图


基础概念

  1. 翻译:面向切面编程,或面向方面编程;
  2. 是OOP的重要补充;
  3. 切面:传统的OOP构建的是对象之间的关系,是一种垂直的关系;假设,OOP程序是一个圆筒,那么与业务或逻辑无关的东西,比如日志,权限等

AOP技术利用一种称为“横切”的技术,解剖封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,这样就能减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处都基本相似。比如权限认证、日志、事务处理。

  1. 几个术语
  • 通知(Advice)
    通知定义了切面是什么以及何时调用,何时调用包含以下几种

Before 在方法被调用之前调用通知

After 在方法完成之后调用通知,无论方法执行是否成功

After-returning 在方法成功执行之后调用通知

After-throwing 在方法抛出异常后调用通知

Around 通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为

  • 切点(PointCut)
    通知定义了切面的什么和何时,切点定义了何处,切点的定义会匹配通知所要织入的一个或多个连接点,我们通常使用明确的类的方法名称来指定这些切点,或是利用正则表达式定义匹配的类和方法名称来指定这些切点。
    切点的格式如下
execution(* com.ganji.demo.service.user.UserService.GetDemoUser (..) )
  • 连接点(JoinPoint)
    连接点是在应用执行过程中能够插入切面的一个点,这个点可以是调用方法时,抛出异常时,甚至是修改一个字段时,切面代码可以利用这些连接点插入到应用的正常流程中,并添加新的行为,如日志、安全、事务、缓存等。
  1. 使用版本:Spring AOP

用法

配置

两种方式:xml和注解(和其他一样,强烈推荐使用第二种,但第一种要了解透彻)

xml方法

在web层,web-inf/dispatcher-servlet.xml中定义切面

<!--定义切面 指定拦截方法时 做什么-->
<bean id="xmlAopDemoUserLog" class="com.ganji.demo.service.aspect.XmlAopDemoUserLog"></bean>
<aop:config>
    <aop:aspect ref="xmlAopDemoUserLog"> <!--指定切面-->
        <!--定义切点-->
        <aop:pointcut id="logpoint" expression="execution(* com.ganji.demo.service.user.UserService.GetDemoUser(..))"></aop:pointcut>
        <!--定义连接点-->
        <aop:before pointcut-ref="logpoint" method="beforeLog"></aop:before>
        <aop:after pointcut-ref="logpoint" method="afterLog"></aop:after>
        <aop:after-returning pointcut-ref="logpoint" method="afterReturningLog"></aop:after-returning>
        <aop:after-throwing pointcut-ref="logpoint" method="afterThrowingLog"></aop:after-throwing>
    </aop:aspect>
</aop:config>

具体用法可以参考

 AOP配置元素 | 描述 
  ------------ | -------------
  `<aop:advisor>` | 定义AOP通知器
  `<aop:after>`  | 定义AOP后置通知(不管该方法是否执行成功)
  `<aop:after-returning>` | 在方法成功执行后调用通知
  `<aop:after-throwing>` | 在方法抛出异常后调用通知
  `<aop:around>` | 定义AOP环绕通知
  `<aop:aspect>` | 定义切面
  `<aop:aspect-autoproxy>` | 定义`@AspectJ`注解驱动的切面
  `<aop:before>` | 定义AOP前置通知
  `<aop:config>` | 顶层的AOP配置元素,大多数的<aop:*>包含在<aop:config>元素内
  `<aop:declare-parent>` | 为被通知的对象引入额外的接口,并透明的实现
  `<aop:pointcut>` | 定义切点

具体使用切面的类,定义如下

package com.ganji.demo.service.aspect;

import org.aspectj.lang.ProceedingJoinPoint;

/**
 * Created by admin on 2015/9/2.
 */
public class XmlAopDemoUserLog {
//    方法执行前通知
    public void beforeLog() {
        System.out.println("开始执行前置通知  日志记录");
    }
//    方法执行完后通知
    public void afterLog() {
        System.out.println("开始执行后置通知 日志记录");
    }
//    执行成功后通知
    public void afterReturningLog() {
        System.out.println("方法成功执行后通知 日志记录");
    }
//    抛出异常后通知
    public void afterThrowingLog() {
        System.out.println("方法抛出异常后执行通知 日志记录");
    }

//    环绕通知
    public Object aroundLog(ProceedingJoinPoint joinpoint) {
        Object result = null;
        try {
            System.out.println("环绕通知开始 日志记录");
            long start = System.currentTimeMillis();

            //有返回参数 则需返回值
            result =  joinpoint.proceed();

            long end = System.currentTimeMillis();
            System.out.println("总共执行时长" + (end - start) + " 毫秒");
            System.out.println("环绕通知结束 日志记录");
        } catch (Throwable t) {
            System.out.println("出现错误");
        }
        return result;
    }
}

注解方法

spring-aop.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>

其中的参数跟切点表达式相关,后面说

对于使用切面的类(Interceptor)标上@Aspect,具体到某个方法,标上相应的注解

package com.ganji.demo.service.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Service;

/**
 * Created by admin on 2015/9/2.
 */
@Aspect
@Service
public class XmlAopDemoUserLog {

// 配置切点 及要传的参数   
    @Pointcut("execution(* com.ganji.demo.service.user.UserService.GetDemoUser(..)) && args(id)")
    public void pointCut(int id)
    {

    }

// 配置连接点 方法开始执行时通知
    @Before("pointCut(id)")
    public void beforeLog(int id) {
        System.out.println("开始执行前置通知  日志记录:"+id);
    }
//    方法执行完后通知
    @After("pointCut(id)")
    public void afterLog(int id) {
        System.out.println("开始执行后置通知 日志记录:"+id);
    }
//    执行成功后通知
    @AfterReturning("pointCut(id)")
    public void afterReturningLog(int id) {
        System.out.println("方法成功执行后通知 日志记录:"+id);
    }
//    抛出异常后通知
    @AfterThrowing("pointCut(id)")
    public void afterThrowingLog(int id) {
        System.out.println("方法抛出异常后执行通知 日志记录"+id);
    }

//    环绕通知
    @Around("pointCut(id)")
    public Object aroundLog(ProceedingJoinPoint joinpoint,int id) {
        Object result = null;
        try {
            System.out.println("环绕通知开始 日志记录"+id);
            long start = System.currentTimeMillis();

            //有返回参数 则需返回值
            result =  joinpoint.proceed();

            long end = System.currentTimeMillis();
            System.out.println("总共执行时长" + (end - start) + " 毫秒");
            System.out.println("环绕通知结束 日志记录");
        } catch (Throwable t) {
            System.out.println("出现错误");
        }
        return result;
    }
}

切点表达式

关于切点表达式,就是出现在配置中的execution后面的表达式,指示符当然不止execution一个

切点指示符

有以下9种切点指示符:execution、within、this、target、args、@target、@args、@within、@annotation

具体如下:

  • execution

execution是一种使用频率比较高比较主要的一种切点指示符,用来匹配方法签名,方法签名使用全限定名,包括访问修饰符(public/private/protected)、返回类型,包名、类名、方法名、参数,其中返回类型,包名,类名,方法,参数是必须的,如下面代码片段所示:

@Pointcut("execution(public String org.baeldung.dao.FooDao.findById(Long))")

上面的代码片段里的表达式精确地匹配到FooDao类里的findById(Long)方法,但是这看起来不是很灵活。假设我们要匹配FooDao类的所有方法,这些方法可能会有不同的方法名,不同的返回值,不同的参数列表,为了达到这种效果,我们可以使用通配符。如下代码片段所示:

@Pointcut("execution(* org.baeldung.dao.FooDao.*(..))")

第一个通配符匹配所有返回值类型,第二个匹配这个类里的所有方法,()括号表示参数列表,括号里的用两个点号表示匹配任意个参数,包括0个

  • within

使用within切点批示符可以达到上面例子一样的效果,within用来限定连接点属于某个确定类型的类。如下面代码的效果与上面的例子是一样的:

@Pointcut("within(org.baeldung.dao.FooDao)")

我们也可以使用within指示符来匹配某个包下面所有类的方法(包括子包下面的所有类方法),如下代码所示:

@Pointcut("within(org.baeldung..*)")
  • this 和 target

this用来匹配的连接点所属的对象引用是某个特定类型的实例,target用来匹配的连接点所属目标对象必须是指定类型的实例;那么这两个有什么区别呢?原来AspectJ在实现代理时有两种方式:

1、如果当前对象引用的类型没有实现自接口时,spring aop使用生成一个基于CGLIB的代理类实现切面编程

2、如果当前对象引用实现了某个接口时,Spring aop使用JDK的动态代理机制来实现切面编程

this指示符就是用来匹配基于CGLIB的代理类,通俗的来讲就是,如果当前要代理的类对象没有实现某个接口的话,则使用this;target指示符用于基于JDK动态代理的代理类,通俗的来讲就是如果当前要代理的目标对象有实现了某个接口的话,则使用target.:

public class FooDao implements BarDao {
    ...
}

比如在上面这段代码示例中,spring aop将使用jdk的动态代理来实现切面编程,在编写匹配这类型的目标对象的连接点表达式时要使用target指示符, 如下所示:

@Pointcut("target(org.baeldung.dao.BarDao)")

如果FooDao类没有实现任何接口,或者在spring aop配置属性:proxyTargetClass设为true时,Spring Aop会使用基于CGLIB的动态字节码技为目标对象生成一个子类将为代理类,这时应该使用this指示器:

@Pointcut("this(org.baeldung.dao.FooDao)")
  • 参数

参数指示符是一对括号所括的内容,用来匹配指定方法参数:

@Pointcut("execution(* *..find*(Long))")

这个切点匹配所有以find开头的方法,并且只一个Long类的参数。如果我们想要匹配一个有任意个参数,但是第一个参数必须是Long类的,我们这可使用下面这个切点表达式:

@Pointcut("execution(* *..find*(Long,..))")
  • @Target

这个指示器匹配指定连接点,这个连接点所属的目标对象的类有一个指定的注解:

@Pointcut("@target(org.springframework.stereotype.Repository)")
  • @args

这个指示符是用来匹配连接点的参数的,@args指出连接点在运行时传过来的参数的类必须要有指定的注解,假设我们希望切入所有在运行时接受实@Entity注解的bean对象的方法:

@Pointcut("@args(org.baeldung.aop.annotations.Entity)")
public void methodsAcceptingEntities() {}

为了在切面里接收并使用这个被@Entity的对象,我们需要提供一个参数给切面通知:JointPoint:

@Before("methodsAcceptingEntities()")
public void logMethodAcceptionEntityAnnotatedBean(JoinPoint jp) {
    logger.info("Accepting beans with @Entity annotation: " + jp.getArgs()[0]);
}
  • @within

这个指示器,指定匹配必须包括某个注解的的类里的所有连接点:

@Pointcut("@within(org.springframework.stereotype.Repository)")

上面的切点跟以下这个切点是等效的:

@Pointcut("within(@org.springframework.stereotype.Repository *)")
  • @annotation

这个指示器匹配那些有指定注解的连接点,比如,我们可以新建一个这样的注解@Loggable:

@Pointcut("@annotation(org.baeldung.aop.annotations.Loggable)")
public void loggableMethods() {}

我们可以使用@Loggable注解标记哪些方法执行需要输出日志:

@Before("loggableMethods()")
public void logMethod(JoinPoint jp) {
    String methodName = jp.getSignature().getName();
    logger.info("Executing method: " + methodName);
}

参数表达式

一般格式是切点指示符+(参数表达式)

举个栗子

* org.baeldung.dao.FooDao.*(..)
所有返回值 FooDao类下的所有方法的(所有入参)

* *..find*(Long,..)
所有返回值 所有以find开头的方法,且该方法第一个参数是Long类型

参数表达式根据不同指示符,指示的略有不同,后续要再补充下


原理

一句话叫做动态代理,具体的话需要了解代理、静态代理、动态代理,java提供的代理;

以不打扰纵向流程,然后加日志为目标/例子;以现实生活中的代理为对照,理解代理的原理和机制吧;

代理的核心实现机制,就是实现同一个接口(委托方和代理方)

静态代码举例:

public interface IHello {
    /**
     * 业务方法
     * @param str
     */
    void sayHello(String str);
}

public class Hello implements IHello{
    @Override
    public void sayHello(String str) {
        System.out.println("hello "+str);
    }
    
}

public class ProxyHello implements IHello{    
    private IHello hello;    
    public ProxyHello(IHello hello) {
        super();
        this.hello = hello;
    }
    @Override
    public void sayHello(String str) {
        Logger.start();//添加特定的方法
        hello.sayHello(str);
        Logger.end();
    }

}

public class Logger {
    public static void start(){
        System.out.println(new Date()+ " say hello start...");
    }
    
    public static void end(){
        System.out.println(new Date()+ " say hello end");
    }
}

public class Test {
    public static void main(String[] args) {
        IHello hello = new ProxyHello(new Hello());//如果我们需要日志功能,则使用代理类
        //IHello hello = new Hello();//如果我们不需要日志功能则使用目标类
        hello.sayHello("明天");    
    }
}

感觉动态代理就是为了不这么麻烦,用反射实现的;


其他注意事项(持续更新)

  • @Around的参数是ProceedingJoinPoint,@Befor、@After是JointPoint
  • AOP目的:我们需要为部分对象引入公共部分的时候
  • AOP应用场景:

Authentication 权限

Caching 缓存

Context passing 内容传递

Error handling 错误处理

Lazy loading 懒加载

Debugging  调试

logging, tracing, profiling and monitoring 记录跟踪 优化 校准

Performance optimization 性能优化

Persistence  持久化

Resource pooling 资源池

Synchronization 同步

Transactions 事务

  • @After拿不到出参;
  • @AfterReturning写法会独特一些
@AfterReturning(value = "execution(* com.shit.Class.user(..)",returning="retVal")
public RetClass onChange(JointPoint jp,Object retVal){
   RetClass retClass = (RetClass)retVal;
   ...
}
  • 只有能够被代理的类,才能被增强,才能被切——一个类中调用的自身的方法,是不能被切到的——简单的改法,让他成为代理,可以是内部代理类
public class appClass extends ClassApp {

   @Resource
   private RemoteProxy remoteProxy;

   @Override
   public RobotAnswerResult responseMessage(UserRequest request) {
       remoteProxy.getResponse(this, request);
       ...
   }
   
   
   
   @Component
   public static class RemoteProxy {
   
       public void getResponse(ClassApp classApp , UserRequest request) {
         
       }
   }
}

参考文章

原文地址:https://www.cnblogs.com/andy1202go/p/9844720.html