额外功能指在核心业务实现上拓展出的一些辅助功能,比如日志、性能监测等。
我们先来看如下案例:
class UserServiceImpl implements UserService{
public boolean login(String name, String passWord) {
System.out.println("***** login *****");
return true;
}
}
需求:
在login
方法中增加记录日志的功能
常规设计:
class UserServiceImpl implements UserService{
public boolean login(String name, String passWord) {
//这里简化了日志写法,只为了传达思想
System.out.println("--- log ---");
System.out.println("***** login *****");
return true;
}
}
从运行结果来看,我们达到了预期的设计要求,在原始方法基础上添加了打印日志的额外功能。
思考:
日志与核心业务放在一块编码,是否存在耦合性问题?是否存在暴露核心业务问题?
如果想增加额外功能,那么就要修改代码,这样是否会降低维护性?
显然上述问题的答案都是肯定的,直接在核心业务上加入额外功能确实不是一个优秀的设计思想
带着上述问题,我们来学习代理设计模式,在学习之前,先引入生活中房东和顾客的案例。
房客作为消费者,想买房
房东作为房屋出租者,负责签合同,收钱
二者的矛盾在于房客想要进一步获得房子的信息,而房东只想签合同,不想干其他事
这里房客想要进一步获得房子信息就是额外功能的体现
而房东又不想实现这个额外功能
所以引入第三个类,中介类
中介在自己的方法体内定义了额外功能,并通过调用房东的方法也实现了签合同付钱的功能
中介的出现可谓解决了二者之间的矛盾
那么回到我们的需求,借鉴现实生活中的解决方式,我们也提出了一个类似中介一样的中间类,下面我们继续梳理下这个中间类应该实现哪些功能?
带着上述两点总结,我们实现下这个中间类。
class UserServiceProxy implements UserService{
private UserServiceImpl userService = new UserServiceImpl();
@Override
public boolean login(String name, String passWord) {
System.out.println("--- log ---");
return userService.login(name, passWord);
}
}
运行结果:
上述的设计思想就是代理模式设计思想,我们提到的中间类即代理类,需要指出的是,我们所指的是静态代理类。该设计模式解决了我们在前面提到的关于耦合性的问题。
小结:
代理类的优点:
代理模式能将代理对象与真实被调用的目标对象分离。
一定程度上降低了系统的耦合度,扩展性好。
可以起到保护目标对象的作用。
可以对目标对象的功能增强。
思考:
一个项目中的核心业务往往很多,如果我们对每个核心业务都通过静态代理模式来扩展额外功能的话,就相当于增加了一倍数量的类,不仅大量增加了静态类文件数量导致项目不易管理,同时也使得项目维护性差,因为如果修改额外功能的话,对应的代理类都需要修改。
动态代理的整体思路和静态代理是吻合的,都是通过一个第三方代理类来增加额外功能,但是区别在于静态代理类是我们手动创建的,而动态代理类采用了动态字节码生成技术(跳过.java
文件,直接生成.class
文件),在JVM
运行过程中动态创建代理类,这样就解决了静态代理造成的大量静态文件生成的问题。
回顾
JVM
创建一个普通类的过程:
虚拟机将.java
文件通过类加载机制编译成.class
文件,通过类的信息在堆区实例化一个对象,并在栈帧中创建对应的引用指向该对象。
而动态代理类显然是没有.java
文件的,因为我们并没有对动态代理类有过任何编码,显然如何生成.class
文件是动态代理的核心思想。
JDK动态代理的核心思想是如下API
:
Proxy.newProxyInstance(classloader, interfaces, invocationhandler);
classloader
表示类加载器,毫无疑问任何想要创建对象的动作都必须有类加载器,然而由于缺少.Java
文件,所以无法直接定位到某个加载器,所以这里的classloader
主要表示调用别的类的加载器,由双亲委派模型我们知道只要调用同一级别或者更下面的类加载器也是可以加载自己的。
interfaces
表示接口,这里的接口是和原始类对应的,在静态代理类中我们强调原始类和代理类应该具有相同的接口,动态代理也不例外。
invocationhandler
是整个动态代理的核心思想。
作用:用于书写额外功能,额外功能可以运行在原始方法执行前/执行后/执行前后/抛出异常时。
核心方法:接口重载了invoke
方法
Object invoke(Object proxy, Method method, Object[] args){
//...
}
参数解读:
Object
指的是原始方法的返回值proxy
指的是代理对象,这个我们一般用不到args
表示原始方法的参数method
表示实现的额外方法method
中有一个invoke
方法,起到方法拦截的作用:
//传入原始方法的对象,方法参数
Object invoke(Object obj, Object.. args){
//...
//
Object obj = invocation.proceed();
return obj;
}
相当于调用了一次原始方法。
有了上述参数,我们就可以考虑向invocationhandler
中的invoke
方法写入具体的内容。
具体思路和静态代理的思路类似,在方法中调用原始方法,在原始方法执行前后加入额外功能。
Object invoke(Object proxy, Method method, Object[] args){
System.out.println("***** log *****");
Object ret = method.invoke(UserService, args);
return ret;
}
这样该接口就实现了额外功能注入的功能。
此时我们介绍完了所有的参数,下面将JDK动态代理一次完整的调用方法补全:
//JDK创建动态代理
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("******* proxy log *******");
//原始方法运行
Object obj = method.invoke(userService, args);
return obj;
}
};
UserService userService1 = (UserService) Proxy.newProxyInstance(TestJDK.class.getClassLoader(), userService.getClass().getInterfaces(), handler);
此时返回的
userService1
就是动态代理对象,包含了原始方法和额外功能。
CGlib
动态代理方式和JDK
动态代理方式基本吻合,只不过JDK
动态代理保证了动态代理于原始类实现相同的接口,而CGlib
动态代理主要针对原始类没有实现接口的情况,通过继承原始类达到实现所有原始方法的目的。
public static void main(String[] args) {
//1.创建原始对象
UserService userService = new UserService();
//2.通过cglib创建动态代理对象
Enhancer enhancer = new Enhancer();
//等同于JDK动态代理的类加载器
enhancer.setClassLoader(TestCglib.class.getClassLoader());
等同于JDK动态代理的接口
enhancer.setSuperclass(userService.getClass());
//等同于JDK动态代理的invocationhandler接口
MethodInterceptor interceptor = new MethodInterceptor() {
//等同于invoke
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("--- cglib log ---");
Object obj = method.invoke(userService, args);
return obj;
}
};
enhancer.setCallback(interceptor);
UserService userService1 = (UserService) enhancer.create();
}
目前为止,我们了解了如何手动创建动态代理,但是Spring
是如何创建动态代理的呢?
在了解Spring
创建动态代理前,我们应该先了解Spring的对象生命周期,探索一个对象在Spring
中从创建到销毁的过程。
以下是Bean
生命周期简化版本,只为了突出动态代理在Spring
中的实现:
简单总结如下:
BeanPostProcessor
接口,进行前置加工BeanPostProcessor
接口,进行后置加工,在该步骤中调用动态代理加工对象,返回动态代理类id
调用的是加工后的动态代理类也就是说,动态代理是在后置加工阶段完成的,即动态代理是将原始类加工获得的,经加工后,也就无法再定位到原始类,或者说原始类变成了动态代理类,这也起到了进一步封装保护的作用。
通过后置加工生成动态代理类的编码如下:
public class ProxyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("--- new log ---");
Object obj = method.invoke(bean, args);
return obj;
}
};
return Proxy.newProxyInstance(ProxyBeanPostProcessor.class.getClassLoader(), bean.getClass().getInterfaces(), handler);
}
}
创建完编码,默认是对所有方法都执行该后置加工的,即为所有原始方法都增加了额外功能,那么如何将后置加工应用于特定的方法呢?这就要引入切入点的概念。
切入点决定了额外功能作用的原始方法
<aop:pointcut id="pc" expression="execution(* *(..))"/>
exection(* *(..))
表示匹配所有方法
切入点函数—execution()
切入点表达式—* *(..)
具体的切入点表达式语法及其他写法不在本文赘述。
上述就是
AOP
编程的核心思想
AOP
:面向切面编程
切面:切入点+额外方法
也可以说AOP
编程就是Spring
动态代理开发