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

思惟导图


基础概念

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

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

  4. 几个术语web

  • 通知(Advice)
    通知定义了切面是什么以及什么时候调用,什么时候调用包含如下几种

Before 在方法被调用以前调用通知正则表达式

After 在方法完成以后调用通知,不管方法执行是否成功spring

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

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在实现代理时有两种方式:

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

二、若是当前对象引用实现了某个接口时,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) {
         
       }
   }
}

参考文章

相关文章
相关标签/搜索