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接口
关于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
***