12 分钟搞懂 静(动)态代理
作 者:王久一
用 时:12 min
静态代理
接下来我们实现简单的加减乘除的案例,并且通过静态代理的方式给每个运算方法加上日志的功能,即打印出:"你输入的参数为a和b,相加结果为c"。
1. 案例截屏
2. AtService.java
public interface AtService {
int add(int a,int b);
int min(int a,int b);
int mul(int a,int b);
int div(int a,int b);
int add(int a,int b,int c);
}
3. AtServiceImpl.java
public class AtServiceImpl implements AtService {
@Override
public int add(int a, int b) {
int result = a + b;
return result;
}
@Override
public int min(int a, int b) {
int result = a - b;
return result;
}
@Override
public int mul(int a, int b) {
int result = a * b;
return result;
}
@Override
public int div(int a, int b) {
int result = a / b;
return result;
}
@Override
public int add(int a, int b, int c) {
int result = a + b + c;
return result;
}
}
上方类称为委托类,可以认为它是我们核心的业务代码,如果想加入日志功能怎么办呢?显然在核心业务代码上直接做修改是不合适的。我们需要在核心代码的基础上添加日志功能,并且在不修改核心代码的前题下我们该怎么做?这时候静态代理就该上场了,创建静态代理类:
4. AtLogServiceImpl.java
public class AtLogServiceImpl implements AtService {
private AtService atService = new AtServiceImpl();
@Override
public int add(int a , int b ) {
int result = atService.add(a , b);
System.out.println("你输入的参数为"+a+"和"+b+";相加结果为"+result);
return result;
}
@Override
public int min(int a , int b ) {
int result = atService.min(a , b);
System.out.println("你输入的参数为"+a+"和"+b+";相减结果为"+result);
return result;
}
@Override
public int mul(int a , int b ) {
int result = atService.mul(a , b);
System.out.println("你输入的参数为"+a+"和"+b+";相乘结果为"+result);
return result;
}
@Override
public int div(int a , int b ) {
int result = atService.div(a , b);
System.out.println("你输入的参数为"+a+"和"+b+";相除结果为"+result);
return result;
}
@Override
public int add(int a , int b , int c ) {
int result = add(int a, int b, int c);
System.out.println("你输入的参数为"+a+"和"+b+"和"+c+";相加结果为"+result);
return result;
}
}
上方类称为代理类,代理类和委托类实现了相同的接口,在代理类中我们首先创建了委托类(核心代码)的实例,在每个方法中调用委托类对应的函数,获取到返回值来打印日志。这样就在没改变原有代码的基础上实现了日志功能。
5. SpringTest.java
public class SpringTest {
@Test
public void add(){
AtService atService = new AtLogServiceImpl();
int result = atService.add(1 , 2);
System.out.println(result);
}
}
注意这里的实现类是new的AtLogServiceImpl()代理类,而不是我们的核心业务代码。
6. 打印结果
通过静态代理我们解决了在不更改核心业务代码的前题下是实现了日志功能,但是静态代理还有一些缺点。
静态代理缺点
代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。
代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。
静态代理一个代理只能代理一种类型,而且是在编译器就已经确定被代理的对象。有没有其他解决方案能够弥补这个确定?
当然有,那就是:动态代理。动态代理是在运行时,通过反射机制实现动态代理,并且能够代理各种类型的对象,比如上面的加减乘除的例子:对于静态代理而言,在代理类中分别实现了加减乘除的方法来给委托类相应的方法添加日志功能,而动态代理可理解为:我们只在代理类写了一个方法就可以操纵委托类的所有方法。
动态代理
在Java中要想实现动态代理机制,需要java.lang.reflect.InvocationHandler接口和 java.lang.reflect.Proxy 类的支持,下方我们再使用一个案例来解释什么是动态代理,案例实现的功能是在进行addUser管理用户等操作前后分别打印出"执行前"、"执行后"的日志。
利用JDK实现动态代理非常简单,可以理解为手动实现Spring 的 AOP功能。大体思路是这样的:首先实现目标类,即接口+实现类,然后创建一个切面类,切面类中存放通知,也就是说切面类有两个方法分别是输出"执行前"、"执行后"。
接着编写工程类来生成代理,在工厂类中我们就需要把目标类和切面类"融合"在一起,实现我们相应的功能(这句话需要有Spring基础才可理解,不懂直接看代码即可。):
1. 目标类:
public interface UserService {
public void addUser();
public void updateUser();
public void deleteUser();
}
上方只是接口,而其实现类就是简单打印方法名,如:addUser()执行输出结果为:"addUser",这里不再罗列代码。
2. 切面类:
public class MyAspect {
public void before(){
System.out.println("执行前");
}
public void after(){
System.out.println("执行后");
}
}
我们的目标就是在 添加用户addUser() 函数运行 之前打印 “执行前”,之后打印“执行后”.
3. 工厂(生成代理)
public class MyBeanFactory {
public static UserService createService(){
//1 目标类
final UserService userService = new UserServiceImpl();
//2切面类
final MyAspect myAspect = new MyAspect();
//3 代理类:将目标类和切面类结合
UserService proxService = (UserService)Proxy.newProxyInstance(
//通过当前类来获取类加载器
MyBeanFactory.class.getClassLoader(),
userService.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method,Object[] args) throws Throwable {
//前执行
myAspect.before();
//执行目标类的方法
Object obj =method.invoke(userService, args);
//后执行
myAspect.after();
return obj;
}
});
return proxService;
}
}
上面的函数,使用 Proxy.newProxyInstance函数将目标类和切面类进行结合,此函数参数什么意思呢 ?下面对三个参数简单解释:
Proxy.newProxyInstance(loader,interfaces ,InvocationHandler)
loader
loader:类加载器
我们知道动态代理类是在运行时创建的,其之前内存中是没有的,必须通过类加载器加载到内存。我们通过 当前类.class.getClassLoader() 来获取类加载器。
interfaces
interfaces:代理类需要实现的所有接口
我们通过 目标类实例.getClass().getInterfaces() 来获取当前目标类的所有接口,但是此方式没办法获取到父元素的接口。
InvocationHandler:处理类
这是一个接口必须进行实现,这里我们是通过匿名内部类的方式来实现,
在其实现类中提供了一个invoke()方法,此方法的作用是:在代理类的每一个方法执行时都会调用一次invoke()方法,此方法有三个参数简单介绍。
Object proxy代理对象,Method method:代理对象当前执行的方法的描述对象,Object[] args:方法的实际参数。
对于Proxy.newProxyInstance()方法还不明白,进行图文解释:首先我们第二个参数是传入了一些接口用来生成代理类,有了接口我们就有了规范,在代理类中就有了接口相应的方法,如图:
代理类没有任何功能,它只是把别的类组合起来。代理类中每一个方法执行时都会调用一次invoke()方法,第一个参数是代理对象,代理对象就是当前的代理类,所以第一个参数为invoke(this,*,*)。
第二参数为方法,自己调用的invoke()方法,所以第二个参数为:invoke(this,addUser,*),通过反射把adddUser方法对应的对象传了过去,而对应的方法也有一些参数,这写参数通过第三个方法传递过去。
其实invoke()方法执行的时候就是调用的处理类InvocationHandler()中的相应方法,此类中就要把切面类和目标对象组合在一起,如图:
4. 测试:
@Test
public void demo01(){
UserService userService = MyBeanFactory.createService();
userService.addUser();
userService.updateUser();
userService.deleteUser();
}
5. 执行结果:
总结
静态代理由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的.class文件就已经存在了。动态代理在程序运行时运用反射机制动态创建而成。
如果喜欢本文章,欢迎转发、收藏。由于此订阅号换了个皮肤,系统自动取消了读者的公众号置顶。导致用户接受文章不及时。您可以打开订阅号「Web项目聚集地」,选择置顶(标星)公众号,重磅干货,第一时间送达!
推荐阅读
4. 5.22:黑客情人节 !