Spring 5 - Spring AOP 架构

概念

  • Joinpoints: 是程序执行中的一个点,比如调用一个方法,方法调用它自己,类初始化和对象实例的生成等。Joinpoints是AOP的核心概念,在程序里定义可以插入附加逻辑的点
  • Advice:在特定的joinpoint执行的代码就是advice,由你的类的方法定义。有很多类型的advice,比如before,
    它在joinpoint之前执行;比如after,它在joinpoint之后执行
  • Pointcuts:是advice应该被执行时候的joinpoints的集合。通过增加pointcuts,可以更细粒度地控制advice如何
    作用于你程序的组件。一个典型的joinpoint是一个方法调用,或者特定类的所有方法调用的集合。你可以在复杂的关系中
    合成pointcuts,限制advice的执行
  • Aspects:封装在一个类中的advice和pointcuts的组合
  • Weaving:把aspects插入程序代码的适当位置的过程。对于编译时AOP,weaving通常在build时候生成;对于运行时AOP,weaving过程在运行时动态执行。AspectJ支持另一个机制-load-time weaving (LTW)-当类被加载的时候,拦截底层的JVM类加载器,把weaving提供给字节码
  • Target:被AOP修改的对象。通常,目标对象就是advised对象
  • Introduction:通过附加的方法或者属性,修改对象的结构的过程。你可以使用introduction AOP,让任何对象实现一个特定的接口,而不用对象的类实现该接口

Spring AOP基于代理。当你增加一个类的advised的实例的时候,你必须使用ProxyFactory增加类的代理实例,先给它提供所有的构建代理的aspects。当然,一般情况下,你不用直接使用ProxyFactory,而是声明AOP配置机制(ProxyFactoryBean、aop名称空间和AspectJ的注解)。为了理解原理,我们先通过编程增加代理。
在运行时,Spring分析bean的定义,动态生成代理bean。不直接调用目标bean,而是调用被注入的代理bean。代理bean分析运行状况(joinpoint、pointcut或者advice)注入合适的内容。Spring支持两种代理实现:JDK动态代理和CGLIB代理。默认地,当要被advised的目标对象实现了一个接口,就使用JDK动态代理生成目标的代理实例。否则,如果目标对象没实现一个接口,就使用CGLIB代理。一个主要原因是,JDK动态代理只能处理接口。

Spring AOP的一个更明显的简化是,它只支持一个joinpoint类型:方法调用。而AspectJ支持更多的joinpoints。方法调用joinpoint是最有用的joinpoint。
Spring AOP的aspect是指实现了Advisor接口的类的实例。Spring提供了方便的Advisor实现,你可以在程序里重新使用。Advisor有两个子接口:PointcutAdvisor和IntroductionAdvisor。所有的Advisor实现都实现了PointcutAdvisor,它使用pointcuts控制应用于joinpoints的advice。

Spring的Advice类型

Advice Name Interface Description
Before org.springframework.aop.MethodBeforeAdvice 在joinpoint执行前,执行自定义过程。因为joinpoint总是方法调用,实质上允许你在方法执行前执行预处理。Before advice可以完全访问方法调用的目标以及传给方法的参数,但是无法控制方法本身的执行。如果在advice前抛了异常,拦截器链(以及目标方法)的执行被终止,异常被传播回拦截器链
After-Returning org.springframework.aop.AfterReturningAdvice 在joinpoint的方法调用执行完成并且已经有返回值以后,执行After-returning advice。它可以访问方法调用的目标,传给方法的参数和返回值。如果方法抛了异常,就不调用advice,异常被传给调用栈。
After(finally) org.springframework.aop.AfterAdvice 方法正常执行完,才调用After-returning advice。而after(finally)无论如何都会执行,甚至在方法抛异常的时候也会执行。
Around org.aopalliance.intercept.MethodInterceptor 使用AOP联盟的方法拦截器标准。在方法调用执行前后都可以执行你的advice,你可以控制允许方法调用继续执行的点。你可以选择旁路方法,提供自己的实现。
Throws org.springframework.aop.ThrowsAdvice 在方法调用抛异常后执行。throws advice可以只捕获特定异常,此时,你可以访问抛出异常的方法,传递给方法的参数和调用的目标。
Introduction org.springframework.aop.IntroductionInterceptor introductions是特殊类型的拦截器。使用这样的拦截器,可以指定advice介绍的方法的实现。

Advice接口

Interfaces for Spring advice types

关于ProxyFactory类

ProxyFactory控制代理增加过程。它有两个重要方法,setTarget()方法指定目标对象,addAdvice()方法增加Advice。
在ProxyFactory内部,由DefaultAopProxyFactory的实例(实际使用JdkDynamicAopProxy或者CglibAopProxy)增加相应的代理。
addAdvice()方法把你传的advice放进DefaultPointcutAdvisor的一个实例,DefaultPointcutAdvisor是PointcutAdvisor的标准实现,默认地,应用于对象的全部方法。

增加 Before Advice

Before advice是Spring支持的最有用的advice类型。它能修改传给方法的参数,能通过抛异常的办法阻止方法执行。下来,我们看看简单的例子,在方法执行前,在控制台写一个消息,其中包含方法的名字。代码是SimpleBeforeAdvice类:

import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.framework.ProxyFactory;

import java.lang.reflect.Method;

public class SimpleBeforeAdvice implements MethodBeforeAdvice {
    public static void main(String... args) {
        Guitarist johnMayer = new Guitarist();
        ProxyFactory pf = new ProxyFactory();
        pf.addAdvice(new SimpleBeforeAdvice());
        pf.setTarget(johnMayer);
        Guitarist proxy = (Guitarist) pf.getProxy();
        proxy.sing();
    }

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("Before '" + method.getName() + "', tune guitar.");
    }

    private static class Guitarist implements Singer {
        private String lyric = "You're gonna live forever in me";

        @Override
        public void sing(){
            System.out.println(lyric);
        }
    }

    interface Singer {
        void sing();
    }
}

Guitarist类很简单,只有一个方法-sing()-在控制台输出lyric。它实现了Singer接口。
可以看到,SimpleBeforeAdvice实现了MethodBeforeAdvice接口,定义了一个方法before()。我们现在使用的是addAdvice()方法提供的默认pointcut,这样就匹配类里的所有方法。before()方法有三个参数:被调用的方法,要传给方法的参数和调用的目标对象。SimpleBeforeAdvice类使用before()方法的Method参数,在控制台写消息,消息包含被调用方法的名字。运行一下,输出是这样的:

Before 'sing', tune guitar.
You're gonna live forever in me

通过Before Advice,实现安全的方法访问

我们实现before advice,在允许方法调用之前,检查用户证书。如果用户证书无效,通过该advice抛异常,这样阻止方法的执行。本例有点简单化,它允许用户使用任何密码鉴权,也只允许一个硬编码的用户访问安全的方法。
先看SecureBean类,它会被安全地执行:

class SecureBean {
    void writeSecureMessage() {
        System.out.println("Every time I learn something new, "
                + "it pushes some old stuff out of my brain");
    }
}

因为本示例需要做用户认证,所以需要有地方保存他们的细节。我们使用UserInfo保存用户证书:

class UserInfo {
    private String userName;
    private String password;

    UserInfo(String userName, String password) {
        this.userName = userName;
        this.password = password;
    }
    public String getPassword() {
        return password;
    }
    public String getUserName() {
        return userName;
    }
}

下来是SecurityManager类,负责认证,并保存他们的证书,供以后检索:

class SecurityManager {
    private static ThreadLocal<UserInfo>
            threadLocal = new ThreadLocal<>();

    void login(String userName, String password) {
        threadLocal.set(new UserInfo(userName, password));
    }
    void logout() {
        threadLocal.set(null);
    }
    UserInfo getLoggedOnUser() {
        return threadLocal.get();
    }
}

程序使用SecurityManager类认证用户,并在之后检索当前已认证的用户的细节。使用login()方法做认证,它增加一个UserInfo对象,保存到ThreadLocal。getLoggedOnUser()方法返回当前认证的用户的信息,如果没有被认证的用户,就返回null。
要检查一个用户是否已经被认证,如果被认证了,就可以访问SecureBean类的方法。所以,我们增加advice,在方法调用前执行,检查用户信息:

import org.springframework.aop.MethodBeforeAdvice;

import java.lang.reflect.Method;

public class SecurityAdvice implements MethodBeforeAdvice {
    private SecurityManager securityManager;

    SecurityAdvice() {
        this.securityManager = new SecurityManager();
    }
    
    @Override
    public void before(Method method, Object[] args, Object target)
            throws Throwable {
        UserInfo user = securityManager.getLoggedOnUser();
        if (user == null) {
            System.out.println("No user authenticated");
            throw new SecurityException(
                    "You must login before attempting to invoke the method: "
                            + method.getName());
        } else if ("John".equals(user.getUserName())) {
            System.out.println("Logged in user is John - OKAY!");
        } else {
            System.out.println("Logged in user is " + user.getUserName()
                    + " NOT GOOD :(");
            throw new SecurityException("User " + user.getUserName()
                    + " is not allowed access to method " + method.getName());
        }
    }
}

SecurityAdvice类在它的构造器里增加了一个SecurityManager的实例。在before()方法里,我们简单地检查,用户名是不是John。如果是,允许用户访问;否则,抛异常。
下面的代码,做测试:

import org.springframework.aop.framework.ProxyFactory;

public class SecurityDemo {
    public static void main(String... args) {
        SecurityManager mgr = new SecurityManager();
        SecureBean bean = getSecureBean();
        mgr.login("John", "pwd");
        bean.writeSecureMessage();
        mgr.logout();
        try {
            mgr.login("invalid user", "pwd");
            bean.writeSecureMessage();
        } catch (SecurityException ex) {
            System.out.println("Exception Caught: " + ex.getMessage());
        } finally {
            mgr.logout();
        }
        try {
            bean.writeSecureMessage();
        } catch (SecurityException ex) {
            System.out.println("Exception Caught: " + ex.getMessage());
        }
    }

    private static SecureBean getSecureBean() {
        SecureBean target = new SecureBean();
        SecurityAdvice advice = new SecurityAdvice();
        ProxyFactory factory = new ProxyFactory();
        factory.setTarget(target);
        factory.addAdvice(advice);
        SecureBean proxy = (SecureBean) factory.getProxy();
        return proxy;
    }
}

我们测试了三个场景,只有John通过了验证:

Logged in user is John - OKAY!
Every time I learn something new, it pushes some old stuff out of my brain
Logged in user is invalid user NOT GOOD :(
Exception Caught: User invalid user is not allowed access to method writeSecureMessage
No user authenticated
Exception Caught: You must login before attempting to invoke the method: writeSecureMessage

增加After-Returning Advice

After-returning advice在方法调用返回后执行。既然方法已经执行了,你不能修改传给它的参数。虽然你能读这些参数,你不能修改执行路径,也不能阻止方法执行。而且,你也不能在after-returning advice内修改返回值,但是,你可以抛异常,异常取代了返回值,被送到调用栈。
下面,我们看两个例子。第一个,在方法被调用后,在控制台输出消息。第二个,显示如何使用after-returning advice,给一个方法增加错误检查。考虑一个类,KeyGenerator,生成用于加密的key。很多加密算法都会碰到这样的问题,有些key太弱了(即使不知道key,也容易导出原始消息)。第二个例子用来检查弱key。

import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.framework.ProxyFactory;

import java.lang.reflect.Method;

public class SimpleAfterReturningAdvice implements
        AfterReturningAdvice {
    public static void main(String... args) {
        Guitarist target = new Guitarist();
        ProxyFactory pf = new ProxyFactory();
        pf.addAdvice(new SimpleAfterReturningAdvice());
        pf.setTarget(target);
        Guitarist proxy = (Guitarist) pf.getProxy();
        proxy.sing();
    }

    @Override
    public void afterReturning(Object returnValue, Method method,
                               Object[] args, Object target) throws Throwable {
        System.out.println("After '" + method.getName() + "' put down guitar.");
    }

    private static class Guitarist implements SimpleBeforeAdvice.Singer {
        private String lyric = "You're gonna live forever in me";

        @Override
        public void sing() {
            System.out.println(lyric);
        }
    }

    interface Singer {
        void sing();
    }
}

理想情况下,key生成器会检查弱key,但是,弱key出现几率很小,所以,很多key生成器不做这样的检查。我们现在使用after-returning advice做检查:

class KeyGenerator {
    static final long WEAK_KEY = 0xFFFFFFF0000000L;
    static final long STRONG_KEY = 0xACDF03F590AE56L;

    private Random rand = new Random();

    long getKey() {
        int x = rand.nextInt(3);
        if (x == 1) {
            return WEAK_KEY;
        }
        return STRONG_KEY;
    }
}

不能认为key生成器是安全的。上面代码每三次就有一次机会生产弱key。WeakKeyCheckAdvice类检查getKey()方法返回的是否弱key:

import org.springframework.aop.AfterReturningAdvice;

import java.lang.reflect.Method;

import static spring.aop.KeyGenerator.WEAK_KEY;

public class WeakKeyCheckAdvice implements AfterReturningAdvice {

    @Override
    public void afterReturning(Object returnValue, Method method,
                               Object[] args, Object target) throws Throwable {
        if ((target instanceof KeyGenerator)
                && ("getKey".equals(method.getName()))) {
            long key = ((Long) returnValue).longValue();
            if (key == WEAK_KEY) {
                throw new SecurityException(
                        "Key Generator generated a weak key. Try again");
            }
        }
    }
}

下面我们做测试:

public class AfterAdviceDemo {
    private static KeyGenerator getKeyGenerator() {
        KeyGenerator target = new KeyGenerator();
        ProxyFactory factory = new ProxyFactory();
        factory.setTarget(target);
        factory.addAdvice(new WeakKeyCheckAdvice());
        return (KeyGenerator)factory.getProxy();
    }

    public static void main(String... args) {
        KeyGenerator keyGen = getKeyGenerator();
        for(int x = 0; x < 10; x++) {
            try {
                long key = keyGen.getKey();
                System.out.println("Key: " + key);
            } catch(SecurityException ex) {
                System.out.println("Weak Key Generated!");
            }
        }
    }
}

增加Around Advice

Around advice像是before和after advice的组合。但是有几点不同:你可以修改返回值,也能阻止方法的执行。意思是,使用around advice,你可以用新代码代替方法。Spring很多地方都使用了around advice,比如远方代理支持和事务管理。
首先,我们使用Agent类,看怎么使用基本的方法拦截器在方法调用的两端写一条消息。应该注意的是,MethodInterceptor接口的invoke()方法的参数集和MethodBeforeAdvice、AfterReturningAdvice的不一样。
下面的例子,我们通过advise,得到方法运行时性能的相关信息。特别是,我们想知道方法执行了多长时间。我们使用了Spring的StopWatch。先看看
WorkerBean,它是被观察对象:

class WorkerBean {
    void doSomeWork(int noOfTimes) {
        for (int x = 0; x < noOfTimes; x++) {
            work();
        }
    }

    private void work() {
        System.out.print("");
    }
}

ProfilingInterceptor类使用StopWatch类观察方法调用时长:

public class ProfilingInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        StopWatch sw = new StopWatch();
        sw.start(invocation.getMethod().getName());
        Object returnValue = invocation.proceed();
        sw.stop();
        dumpInfo(invocation, sw.getTotalTimeMillis());
        return returnValue;
    }

    private void dumpInfo(MethodInvocation invocation, long ms) {
        Method m = invocation.getMethod();
        Object target = invocation.getThis();
        Object[] args = invocation.getArguments();
        System.out.println("Executed method: " + m.getName());
        System.out.println("On object of type: " +
                target.getClass().getName());
        System.out.println("With arguments:");
        for (int x = 0; x < args.length; x++) {
            System.out.print("       > " + args[x]);
        }
        System.out.print("\n");
        System.out.println("Took: " + ms + " ms");
    }
}

下来是测试代码:

public class ProfilingDemo {
    public static void main(String... args) {
        WorkerBean bean = getWorkerBean();
        bean.doSomeWork(10000000);
    }

    private static WorkerBean getWorkerBean() {
        WorkerBean target = new WorkerBean();
        ProxyFactory factory = new ProxyFactory();
        factory.setTarget(target);
        factory.addAdvice(new ProfilingInterceptor());
        return (WorkerBean) factory.getProxy();
    }
}

输出是

Executed method: doSomeWork
On object of type: spring.aop.WorkerBean
With arguments:
       > 10000000
Took: 761 ms

增加Throws Advice

Throws advice和after-returning advice类似,它在joinpoint(总是一个方法调用)之后执行,但是,它只在方法抛异常后才执行。如果你使用throws advice,不能忽略抛出的异常,替方法返回一个值。你只能改变异常类型。这是一个相当强大的概念,使得程序开发更简单。当然,你也可以使用throws advice实现跨程序的集中化的错误日志。
ThrowsAdvice接口没定义任何方法,它是简单的marker接口。Spring允许typed throws advice,它允许你定义你的throws advice应该捕获什么异常类型。Spring使用反射技术,通过特定签名的探测方法实现该功能。Spring查找两个不同的方法签名。
先看下面的bean,有两个方法,都简单地抛出不同类型的异常:

class ErrorBean {
    void errorProneMethod() throws Exception {
        throw new Exception("Generic Exception");
    }

    void otherErrorProneMethod() throws IllegalArgumentException {
        throw new IllegalArgumentException("IllegalArgument Exception");
    }
}

再看SimpleThrowsAdvice类,演示了Spring查找throws advice的两个方法签名:

import org.springframework.aop.ThrowsAdvice;
import org.springframework.aop.framework.ProxyFactory;

import java.lang.reflect.Method;

public class SimpleThrowsAdvice implements ThrowsAdvice {
    public static void main(String... args) throws Exception {
        ErrorBean errorBean = new ErrorBean();
        ProxyFactory pf = new ProxyFactory();
        pf.setTarget(errorBean);
        pf.addAdvice(new SimpleThrowsAdvice());
        ErrorBean proxy = (ErrorBean) pf.getProxy();
        try {
            proxy.errorProneMethod();
        } catch (Exception ignored) {
        }
        try {
            proxy.otherErrorProneMethod();
        } catch (Exception ignored) {
        }
    }

    public void afterThrowing(Exception ex) throws Throwable {
        System.out.println("***");
        System.out.println("Generic Exception Capture");
        System.out.println("Caught: " + ex.getClass().getName());
        System.out.println("***\n");
    }

    public void afterThrowing(Method method, Object args, Object target,
                              IllegalArgumentException ex) throws Throwable {
        System.out.println("***");
        System.out.println("IllegalArgumentException Capture");
        System.out.println("Caught: " + ex.getClass().getName());
        System.out.println("Method: " + method.getName());
        System.out.println("***\n");
    }
}

首先,Spring在throws advice内查找一个或者多个叫afterThrowing()的public方法。方法的返回类型不重要,当然最好是void。
SimpleThrowsAdvice类的第一个afterThrowing()方法,只有一个Exception参数。你可以指定任何类型的Exception当参数,这个方法捕获Exception和它的任何子类型,除非某异常有自己的afterThrowing() 方法。
第二个afterThrowing()方法,我们声明了四个参数:抛异常的方法、传给方法的参数、方法调用的目标对象、异常类型。参数顺序很重要,而且必须指定这四个参数。第二个afterThrowing()方法,捕获IllegalArgumentException(和它的子类型)。执行如下:

***
Generic Exception Capture
Caught: java.lang.Exception
***

***
IllegalArgumentException Capture
Caught: java.lang.IllegalArgumentException
Method: otherErrorProneMethod
***

猜你喜欢

转载自blog.csdn.net/weixin_43364172/article/details/84390800