Spring的核心是IOC与AOP,IOC主要是依赖关系的管理,包括依赖查询和依赖注入,在之前关于bean的文章中已经对bean的生命周期做了相对多的分析,基本了解了IOC的原理。在这里我们来探讨下AOP的实现原理。
AOP(面向方面编程),也被称为面向切面编程。AOP技术利用一种称为“横切”的技术,解剖封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,这样就能减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。
AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处都基本相似,比如权限认证、日志、事务处理等功能。
这些概念似乎很难懂,专业术语太多。确实,我第一次学也完全不知道他在讲什么。于是我们还是老规矩,先不看这些概念,自己去实现一个简单的AOP模块来帮助我们理解。实现AOP的技术,主要分为两大类:
采用动态代理技术,编写一个代理类的接口,实现一个真正的代理类,实现该接口,创建一个动态的代理类,实现InvocationHandler接口,并重写invoke()方法,调用Proxy.newProxyInstance()方法生成动态代理对象。
采用静态代理技术,自定义一个代理类(增强类)实现和被代理类(被增强类)相同的接口,在代理类中声明被代理类的对象,在代理类的方法中使用被代理类调用方法。
这张图可以帮助大家大概了解静态代理和动态代理的主要区别,具体的细节我会在下面的实例中讲。
首先,我们需要定义我们的核心业务类的接口。
public interface IHello {
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 Hello hello;
public ProxyHello() {
super();
this.hello = new 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(); //需要日志功能,使用代理类
//IHello hello = new Hello(); //不需要日志功能,直接使用目标类
hello.sayHello("tomorrow");
}
}
这样我们就实现了一个最简单的AOP。但是静态代理的实现会存在一个问题:如果我们像Hello这样的类很多,那么我们就要去写很多个HelloProxy这样的类。很明显这是一件麻烦的事,也不利于后期维护。那么我们就会想办法来改进,这也就出现了动态代理的实现方式。
我们先来看一种简单的基于JDK Proxy的动态代理实现方式。首先,我们依旧先把我们的核心业务接口和目标类写好。
public interface IHello {
void sayHello(String str);
}
public class Hello implements IHello{
@Override
public void sayHello(String str) {
System.out.println("hello " + str);
}
}
注意这个日志的接口,我们传入的参数是一个方法对象
public interface ILogger {
void start(Method method);
void end(Method method);
}
日志实现类也与之前不太一样,这里我们可以手动来获取方法名,这样日志中就可以记录调用的方法的名称。
public class DLogger implements ILogger{
@Override
public void start(Method method) {
System.out.println(new Date() + method.getName() + " say hello start");
}
@Override
public void end(Method method) {
System.out.println(new Date() + method.getName() + " say hello end");
}
}
下面我们来看看这个DynaProxyHello类,很容易发现他是InvocationHandler接口的实现类。首先我们会注意到两个属性,一个是proxy,一个是target,分别代表了我们的调用对象(这个案例中指日志模块)和目标对象。其中bind方法中调用了newProxyInstance方法,他通过字符串化产生一个新的java类,再动态编译返回对象,注意是动态编译。简单来说就是他通过一些传递进来的参数,自己动态模拟写了一个java类,实例化对象之后返回给用户,这个对象就是我们的代理对象。
invoke方法中就是通过反射来调用日志方法和核心方法,我们可以看见start.invoke、method.invoke、end.invoke,因为用了反射所以看起来可能复杂一些,但实际上功能与前面的静态代理并没有什么本质区别。当我们创建了的代理类对象进行方法调用的时候,都会转化为对invoke方法的调用。
public class DynaProxyHello implements InvocationHandler{
private Object proxy; //调用对象
private Object target; //目标对象
public Object bind(Object target,Object proxy){
this.target = target;
this.proxy = proxy;
//生成代理对象
return Proxy.newProxyInstance(this.target.getClass().getClassLoader(), this.target.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Object result = null;
//反射得到调用类
Class clazz = this.proxy.getClass();
//反射得到调用类的Start方法
Method start = clazz.getDeclaredMethod("start", new Class[]{Method.class});
//反射执行start方法
start.invoke(this.proxy, new Object[]{method});
//执行目标类的核心方法
method.invoke(this.target, args);
//反射得到调用类的end方法
Method end = clazz.getDeclaredMethod("end", new Class[]{Method.class});
//反射执行end方法
end.invoke(this.proxy, new Object[]{method});
return result;
}
}
下面来测试一下。
public class Test {
public static void main(String[] args) {
IHello hello = (IHello) new DynaProxyHello().bind(new Hello(), new DLogger()); //需要日志功能,使用代理类
//IHello hello = new Hello(); //不需要日志功能,直接使用目标类
hello.sayHello("tomorrow");
}
}
我们可以发现JDK Proxy版的动态代理有一个问题,就是只能针对接口进行代理(测试类主函数的第一行 IHello hello = (IHello) new DynaProxyHello().bind(new Hello(), new DLogger()); 需要基于IHello接口),如果我们要代理一个普通对象,这个方法是无法使用的。因此我们需要寻找另一种方法。
CGLIB,一个比JDK Proxy的动态代理更加强大的代理工具,底层使用了ASM(一个短小精悍的字节码操作框架)来操作字节码生成新的类,比使用反射效率更高。他可以实现普通对象(无接口)的代理,并且代码更加紧凑易读,下面我们就用CGLIB来实现与上面同样的功能。
首先我们需要导入cglib包,我这里用的是当前的最新版本3.3.0。
<dependency>
<groupId>cglibgroupId>
<artifactId>cglibartifactId>
<version>3.3.0version>
dependency>
然后将之前写过的日志模块的接口和实现类写好。
public interface ILogger {
void start(Method method);
void end(Method method);
}
public class DLogger implements ILogger {
@Override
public void start(Method method) {
System.out.println(new Date() + method.getName() + " say hello start");
}
@Override
public void end(Method method) {
System.out.println(new Date() + method.getName() + " say hello end");
}
}
然后是我们的核心业务类。
public class Hello {
public void sayHello(String str) {
System.out.println("hello " + str);
}
}
紧接着,是最关键的拦截器,这是与上面JDK的动态代理最不一样的地方。这个类要实现MethodInterceptor接口,实际上实现的功能和上面的invoke方法基本一致,我们这里可以先把他当做invoke方法来理解。
public class DynaProxyHello implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
DLogger dLogger = new DLogger();
dLogger.start(method);
Object object = methodProxy.invokeSuper(o, objects);
dLogger.end(method);
return object;
}
}
然后是测试类,这个类明显比较复杂,我们来看看他干了些什么事。首先将目标类Hello设置成父类,然后设置拦截器DynaProxyHello,最后执行enhancer.create()动态生成一个代理类,并从Object强制转型成父类Hello。其中,Enhancer类是CGLib中的一个字节码增强器,它可以方便的对你想要处理的类进行扩展,我们暂时还不需要了解这么细,想弄明白的朋友可以自行Google。
public class Test {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
//继承目标类
enhancer.setSuperclass(Hello.class);
//设置回调
enhancer.setCallback(new DynaProxyHello());
//生成代理对象
Hello hello = (Hello) enhancer.create();
hello.sayHello("tomorrow");
}
}
这样一个基于CGLib的动态代理就写完了,我个人感觉这个过程比基于JDK的动态代理要简单一点,不过涉及的知识点反而更多了,需要去仔细的思考。我们可以看一下刚刚讲的两种动态代理方式的AOP实现的区别:
不卖关子了,实际上Spring AOP就是基于动态代理的。如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy去创建代理对象。而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用CGlib生成一个被代理对象的子类来作为代理。当然,我们也可以设置强制使用CGlib。
2020年7月23日