Spring AOP的实现机制

这是我参与更文挑战的第12天,活动详情查看:更文挑战

Spring AOP属于第二代AOP,采用动态代理机制和字节码生成技术实现。与最初的AspectJ采用编译器将横切逻辑织入目标对象不同,动态代理机制和字节码生成都是在运行期间为目标对象生成一个代理对象,而将横切逻辑织入到这个代理对象中,系统最终使用的是织入横切逻辑的代理对象,而不是真正的目标对象。

为了理解这种差别以及最终可以达到的效果,我们有必要先从动态代理机制的根源—代理模式开始说起

设计模式之代理模式

代理处于访问者与被访问者之间,可以隔离这两者之间的直接交互,访问者与代理打交道就好像在跟被访问者在打交道一样,因为代理通常几乎会全权拥有被代理者的职能,代理能够处理的访问请求就不必要劳烦被访问者来处理了。从这个角度来说,代理可以减少被访问者的负担。另外,即使代理最终要将访问请求转发给真正的被访问者,它也可以在转发访问请求之前或者之后加入特定的逻辑,比如安全访问限制,或者,想房产中介那样抽取一定的中介费等。

在代理模式中,通常涉及4中角色,如下图所示。

IMG_0085(20210621-121214)

ISubject:该接口是对被访问者或者被访问资源的抽象

SubjectImpl:被访问者或者被访问资源的具体实现类

SubjectProxy:被访问者或者被访者资源的代理实现类,该类持有一个ISubject接口的具体实例。在这个场景中,我们要对SubjectImpl进行代理,那么SubjectProxy现在持有的就是SubjectImpl的实例。

Client:表示访问者的抽象角色

SubjectImpl和SubjectProxy都实现了相同的接口ISubject,而SubjectProxy内部持有SubjectImpl的引用。当Client通过request()请求服务时,SubjectProxy将转发该请求给SubjectImpl。从这个角度来说,SubjectProxy反而有多此一举之嫌了。不过,SubjectProxy的作用不只局限于请求的转发,更多时候是对请求添加更多访问限制。SubjectImpl和SubjectProxy直接的调用关系如下图所示:

IMG_0086(20210621-121237)

在将请求转发给被代理对象SubjectImpl之前或者之后,都可以根据情况插入其他处理逻辑,比如在转发之前记录方法执行开始时间,在转发之后记录结束时间,这样就能够对SubjectImpl的request()执行的时间进行检测。

Spring AOP本质上就是采用这种代理机制来实现的,但是,具体实现细节上有所不同,让我们来看看为什么。

假设要对系统中所有的request()方法进行拦截,在每天午夜0点到次日6点之间,request调用不被接受,那么,我们应该为SubjectImpl提供一个ServiceControlSubjectProxy,以添加这种横切逻辑,ServiceControlSubjectProxy定义如下:

public class ServiceControlSubjectProxy implements ISubject {
    private ISubject subject;
    public ServiceControlSubjectProxy(ISubject s){
        this.subject = s;
    }
    public String request(){
        TimeOfDay startTime = new TimeOfDay(0,0,0);
        TimeOfDay endTime = new TimeOfDay(5,59,59);
        TimeOfDay currentTime = new TimeOfDay();
        if (currentTime.isAfter(startTime) && currentTime.isBefore(endTime))
            return null;
        
        String originalResult = subject.request();
        return "Proxy:"+originalResult;
    }
}
复制代码

之后,我们使用该ServiceControlSubjectProxy代替SubjectImpl使用,如以下代码所示:

ISubject target = new SubjectImpl();
ISubject finalSubject = new ServiceControlSubjectProxy(target);
finalSubject.request();
//访问控制相关逻辑已经包含在请求的处理逻辑中
复制代码

但是,系统中可不一定就ISubject的实现类有request()方法,IRequestable接口以及相应实现类可能也有request()方法,它们也是我们需要横切的关注点。IRequestable及其实现类代码如下:

public interface IRequestable{
    void request();
}
//实现类
public class RequestableImpl implements IRequestable{
    public void request(){
        System.out.println("request processed in RequestableImpl");
    }
}
复制代码

为了能够为IRequestable相应实现类也织入以上的横切逻辑,我们又得提供相应的代理对象,代码如下:

public class ServiceControlRequestProxy implements ISubject {
    private IRequestable requestable;
    public ServiceControlRequestProxy(IRequestable requestable){
        this.requestable = requestable;
    }
    public void request(){
        TimeOfDay startTime = new TimeOfDay(0,0,0);
        TimeOfDay endTime = new TimeOfDay(5,59,59);
        TimeOfDay currentTime = new TimeOfDay();
        if (currentTime.isAfter(startTime) && currentTime.isBefore(endTime))
            return ;
        
        requestable.request();
    }
}
复制代码

并且将该代理对象而不是目标对象绑定到系统中,代码如下:

IRequestable target = new RequestableImpl();
IRequestable proxy = new ServiceControlRequestProxy(target);
proxy.request();
复制代码

发现问题了没有?虽然JoinPoint相同(request()方法的执行),但是对应的目标对象类型是不一样的。针对不一样的目标对象类型,我们要为其单独实现一个代理对象。而实践上,这些代理对象所要添加的横切逻辑是一样的。当系统中存在成千上百的符合Pointcut匹配条件的目标对象时且类型都不同,则我们就要为这些目标对象创建成百上千的代理对象!

这种为对应的目标对象创建静态代理的方法,原理上是可行的,但具体应用上存在问题,所以寻找其他方法,以避免刚刚碰到的窘境…

动态代理

使用该机制,我们可以为指定的接口在系统运行期间动态地生成代理对象,从而帮助我们走出最初使用静态代理实现的窘境。

动态代理机制的实现主要由一个类和一个接口组成,即java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。下面,让我们看一下,如何使用动态代理来实现之前的“request服务时间控制”功能。虽然要为ISubject和IRequestable两种类型提供代理对象,但因为代理对象中要添加的横切逻辑是一样的,所以,我们只需要实现一个InvocationHandler就可以了,其定义如下:

public class RequestCtrlInvovationHandler implements InvocationHandler{
	private Object target;
    public RequestCtrlInvovationHandler(Object target){
        this.target = target;
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
        if (method.getName.equals("request")){
            TimeOfDay startTime = new TimeOfDay(0,0,0);
            TimeOfDay endTime = new TimeOfDay(5,59,59);
            TimeOfDay currentTime = new TimeOfDay();
            if (currentTime.isAfter(startTime) && currentTime.isBefore(endTime))
            	return null;
            
            return method.invoke(target, args);
        }
        return null;
    }
}
复制代码

然后,我们就可以使用Proxy类,根据RequestCtrlInvovationHandler的逻辑,为ISubject和IRequestable两种类型生成相应的代理对象实例,代码如下:

ISubject subject = (ISubject)Proxy.newProxyInstance(
	ProxyRunner.class.getClassLoader(),
    new Class[](ISubject.class),
    new RequestCtrlInvovationHandler(new SubjectImpl()));
subject.request();

IRequestable requestable = (IRequestable)Proxy.newProxyInstance(
	ProxyRunner.class.getClassLoader(),
    new Class[](IRequestable.class),
    new RequestCtrlInvovationHandler(new RequestableImpl()));
requestable.request();
复制代码

动态代理虽好,但不能满足所有的需求。因为动态代理机制只能对实现了相应Interface的类使用,如果某个类没有实现任何的Interface,就无法使用动态代理机制为其生成相应的动态代理对象。虽然面向接口编程应该是提倡的做法,但不排除其他的编程实践。对于没有实现任何Interface的目标对象,我们需要寻找其他方式为其动态的生成代理对象。

默认情况下,如果Spring AOP发现目标对象实现了相应Interface,则采用动态代理机制为其生成代理对象实例。而如果目标对象没有实现任何Interface,Spring AOP会尝试使用一个称为CGLIB(Code Generation Library)的开源的动态字节码生成类库,为目标对象生成动态的代理对象实例。

动态字节码生成

使用动态字节码生成技术扩展对象行为的原理是,我们可以对目标对象进行继承扩展,为其生成相应的子类,而子类可以通过覆写来扩展父类的行为,只要将横切逻辑的实现放到子类中,然后让系统使用扩展后的目标对象的子类,就可以达到与代理模式相同的效果了。

但是,使用继承的方式来扩展对象定义,也不能想静态代理模式那样,为每个不同类型的目标对象都单独创建相应的子类。所以,我们要借助于CGLIB这样的动态字节码生成库,在系统运行期间动态地为目标对象生成相应的扩展子类。

为了演示CGLIB的使用以及最终可以达到的效果,我们定义的目标类如下:

public class Requestable{
    public void request(){
        System.out.println("rq in Requestable without implement any interface");
    }
}
复制代码

要对Requestable类进行扩展,首先需要实现一个net.sf.cglib.proxy.Callback。不过更多的时候,我们会直接使用net.sf.cglib.proxy.MethodInterceptor接口(MethodInterceptor扩展了Callback接口)。

public class RequestCtrlCallback implements MethodInterceptor{
    public Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) throws Throwable{
        if (method.getName.equals("request")){
            TimeOfDay startTime = new TimeOfDay(0,0,0);
            TimeOfDay endTime = new TimeOfDay(5,59,59);
            TimeOfDay currentTime = new TimeOfDay();
            if (currentTime.isAfter(startTime) && currentTime.isBefore(endTime))
            	return null;
            
            return proxy.invokeSuper(object, args);
        }
        return null;
    }
}
复制代码

这样,RequestCtrlCallback就实现了对request()方法请求进行访问控制的逻辑。现在我们要通过CGLIB的enhancer为目标对象动态地生成一个子类,并将RequestCtrlCallback中的横切逻辑附加到该子类中,代码如下:

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Requestable.class);
enhance.setCallback(new RequestCtrlCallback());

Requestable proxy = (Requestable)enhancer.create();
proxy.request();
复制代码

通过为enhancer指定需要生成的子类对应的父类,以及Callback实现,enhancer最终为我们生成了需要的代理对象实例。

使用CGLIB对类进行扩展唯一限制就是无法对final方法进行覆写。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享