目录
一:GoF之代理模式
1. 对代理模式的理解
2. 静态代理
3. 动态代理
3.1 JDK动态代理
3.2 CGLIB动态代理
一:GoF之代理模式
1. 对代理模式的理解
生活场景1:牛村的牛二看上了隔壁村小花,牛二不好意思直接找小花,于是牛二找来了媒婆王妈妈;这里面就有一个非常典型的代理模式。牛二不能和小花直接对接,只能找一个中间人。其中王妈妈是代理类,牛儿是目标类。王妈妈代替牛二和小花先见个面。(现实生活中的婚介所)【在程序中,对象A和对象B无法直接交互时】
生活场景2:你刚到北京,要租房子,可以自己找,也可以找链家帮你找;其中链家是代理类,你是目标类。你们两个都有共同的行为:找房子。不过链家除了满足你找房子,另外会收取一些费用的。(现实生活中的房产中介)【在程序中,功能需要增强时】
西游记场景:八戒和高小姐的故事,八戒要强抢民女高翠兰。悟空得知此事之后怎么做的?悟空幻化成高小姐的模样,代替高小姐与八戒会面。其中八戒是客户端程序,悟空是代理类,高小姐是目标类。那天夜里,在八戒眼里,眼前的就是高小姐,对于八戒来说,他是不知道眼前的高小姐是悟空幻化的,在他内心里这就是高小姐,所以悟空代替高小姐和八戒亲了嘴儿;这是非常典型的代理模式实现的保护机制。代理模式中有一个非常重要的特点:对于客户端程序来说,使用代理对象时就像在使用目标对象一样。【在程序中,目标需要被保护时】
业务场景:系统中有A、B、C三个模块,使用这些模块的前提是需要用户登录,也就是说在A模块中要编写判断登录的代码,B模块中也要编写,C模块中还要编写,这些判断登录的代码反复出现,显然代码没有得到复用,可以为A、B、C三个模块提供一个代理,在代理当中写一次登录判断即可!代理的逻辑是:请求来了之后,判断用户是否登录了,如果已经登录了,则执行对应的目标,如果没有登录则跳转到登录页面。【在程序中,目标不但受到保护,并且代码也得到了复用】
(1)代理模式是GoF23种设计模式之一,属于结构型设计模式。
(2)代理模式的作用是:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个客户不想或者不能直接引用一个对象,此时可以通过一个称之为“代理”的第三者来实现间接引用。代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不应该看到的内容和服务或者添加客户需要的额外服务。 通过引入一个新的对象来实现对真实对象的操作或者将新的对象作为真实对象的一个替身,这种实现机制即为代理模式,通过引入代理对象来间接访问一个对象,这就是代理模式的模式动机。
(3)代理模式中的角色:
①代理类(代理主题)
②目标类(真实主题)
③代理类和目标类的公共接口(抽象主题):客户端在使用代理类时就像在使用目标类,不被客户端所察觉,所以代理类和目标类要有共同的行为,也就是实现共同的接口。
(4)代理模式的类图:
(5)代理模式在代码实现上,包括两种形式:
①静态代理。
②动态代理。
2. 静态代理
订单业务接口OrderService类
package com.bjpowernode.proxy.service;
// 订单业务接口
public interface OrderService {
// 生成订单
void generate();
// 修改订单信息
void modify();
// 查看订单信息
void detail();
}
接口的实现类OrderServiceImpl类
其中Thread.sleep()方法的调用是为了模拟操作耗时。
package com.bjpowernode.proxy.service.impl;
import com.bjpowernode.proxy.service.OrderService;
public class OrderServiceImpl implements OrderService {
@Override
public void generate() {
try {
// 模拟订单生成耗时
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已生成...");
}
@Override
public void modify() {
try {
// 模拟修改订单耗时
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已修改...");
}
@Override
public void detail() {
try {
// 模拟查看订单耗时
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("正在查看详情...");
}
}
客户端进行访问测试
package com.bjpowernode.proxy.client;
import com.bjpowernode.proxy.service.OrderService;
import com.bjpowernode.proxy.service.impl.OrderServiceImpl;
public class Test {
public static void main(String[] args) {
OrderService orderService = new OrderServiceImpl();
orderService.generate();
orderService.modify();;
orderService.detail();
}
}
假如项目已上线,并且运行正常,只是客户反馈系统有一些地方运行较慢,要求项目组对系统进行优化;于是项目负责人就下达了这个需求。首先需要搞清楚是哪些业务方法耗时较长,于是让我们统计每个业务方法所耗费的时长?如果是你,你该怎么做呢?
第一种方案:直接修改Java源代码,在每个业务方法中添加统计逻辑,如下:
直接在原来的代码片段中进行修改!
package com.bjpowernode.proxy.service.impl;
import com.bjpowernode.proxy.service.OrderService;
public class OrderServiceImpl implements OrderService {
@Override
public void generate() {
// 执行前计时
long begin = System.currentTimeMillis();
try {
// 模拟订单生成耗时
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已生成...");
// 执行后计时
long end = System.currentTimeMillis();
System.out.println("耗费时长"+(end - begin)+"毫秒");
}
@Override
public void modify() {
// 执行前计时
long begin = System.currentTimeMillis();
try {
// 模拟修改订单耗时
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已修改...");
// 执行后计时
long end = System.currentTimeMillis();
System.out.println("耗费时长"+(end - begin)+"毫秒");
}
@Override
public void detail() {
// 执行前计时
long begin = System.currentTimeMillis();
try {
// 模拟查看订单耗时
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("正在查看详情...");
// 执行后计时
long end = System.currentTimeMillis();
System.out.println("耗费时长"+(end - begin)+"毫秒");
}
}
缺点:需求可以满足,但显然是违背了OCP开闭原则,并且相同的代码写了很多遍;所以这种方案不可取!
第二种方案:再编写一个子类OrderServiceImplSub继承OrderServiceImpl,在子类中重写每个方法,代码如下:
使用继承(泛化)关系,以子类的方式存在,在子类方法中使用super关键子调用父类的方法,耦合度比较高!
package com.bjpowernode.proxy.service.impl;
public class OrderServiceImplSub extends OrderServiceImpl{
@Override
public void generate() {
// 执行前计时
long begin = System.currentTimeMillis();
// 调用父类的方法
super.generate();
// 执行后计时
long end = System.currentTimeMillis();
System.out.println("耗费时长"+(end - begin)+"毫秒");
}
@Override
public void modify() {
// 执行前计时
long begin = System.currentTimeMillis();
// 调用父类的方法
super.modify();
// 执行后计时
long end = System.currentTimeMillis();
System.out.println("耗费时长"+(end - begin)+"毫秒");
}
@Override
public void detail() {
// 执行前计时
long begin = System.currentTimeMillis();
// 调用父类的方法
super.detail();
// 执行后计时
long end = System.currentTimeMillis();
System.out.println("耗费时长"+(end - begin)+"毫秒");
}
}
这种方式也可以解决,但是存在两个问题:
①缺点:虽然解决了OCP开闭原则,但是采用了继承的方式,导致代码之间的耦合度较高。
②缺点:代码也没有得到复用。
第三种方案:使用代理模式(静态代理)
使用关联关系,以属性的方式存在,代理对象和目标对象实现同一个接口,并把目标对象通过有参构造传过来,耦合度比继承低!
代理对象
前面最初编写的OrderService接口就是公共接口、OrderServiceImpl就是目标对象,现在就需要编写一个代理对象;代理对象和目标对象要具有相同的行为,就要实现同一个或同一些接口!
package com.bjpowernode.proxy.service.impl;
import com.bjpowernode.proxy.service.OrderService;
// 代理对象(代理对象和目标对象要具有相同的行为,就要实现同一个或同一些接口)
// 客户端在使用代理对象的时候就像在使用目标对象一样。
public class OrderServiceProxy implements OrderService {
// 将目标对象作为代理对象的一个属性,这种关系叫做关联关系,比前面的继承关系的耦合度低。
// 注意:这里要写一个公共接口类型,因为公共接口耦合度低。
private OrderService target;
// 提供构造方法进行赋值
public OrderServiceProxy(OrderService target) {
this.target = target;
}
@Override
public void generate() { // 代理方法
// 增强
long begin = System.currentTimeMillis();
// 调用目标对象的目标方法
target.generate();
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
}
@Override
public void modify() {// 代理方法
// 增强
long begin = System.currentTimeMillis();
// 调用目标对象的目标方法
target.modify();
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
}
@Override
public void detail() {// 代理方法
// 增强
long begin = System.currentTimeMillis();
// 调用目标对象的目标方法
target.detail();
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
}
}
编写测试
先创建一个目标对象,然后把目标对象作为属性传到代理对象当中,因为我们在代理对象当中的构造方法中使用的是它们共有的接口OrderService作为属性参数;所以就可以通过代理对象的代理方法完成操作!
package com.bjpowernode.proxy.client;
import com.bjpowernode.proxy.service.OrderService;
import com.bjpowernode.proxy.service.impl.OrderServiceImpl;
import com.bjpowernode.proxy.service.impl.OrderServiceProxy;
public class Test {
public static void main(String[] args) {
// 创建目标对象
OrderService target = new OrderServiceImpl();
// 创建代理对象
OrderService proxy = new OrderServiceProxy(target);
// 调用代理对象的代理方法
proxy.generate();
proxy.modify();
proxy.detail();
}
}
①优点:解决了OCP问题,采用代理模式的关联关系,可以降低耦合度!
②缺点:类爆炸,假设系统中有1000个接口,那么每个接口都需要对应代理类,这样类会急剧膨胀;仍然未解决代码复用问题,不好维护。
③怎么解决类爆炸问题?
可以使用动态代理来解决这个问题;动态代理还是代理模式,只不过添加了字节码生成技术,可以在内存中为我们动态的生成一个class字节码,这个字节码就是代理类。
3. 动态代理
(1)在程序运行阶段,在内存中动态生成代理类,被称为动态代理,目的是为了减少代理类的数量和解决代码复用的问题!
(2)在内存当中动态生成类的技术常见的包括:
①JDK动态代理技术:只能代理接口。
②CGLIB动态代理技术:CGLIB(Code Generation Library)是一个开源项目。是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。它既可以代理接口,又可以代理类,底层是通过继承的方式实现的。性能比JDK动态代理要好(底层有一个小而快的字节码处理框架ASM)
③Javassist动态代理技术:Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。(前面MyBatis已经讲过)
OrderService接口
package com.bjpowernode.dynamic.proxy;
// 订单业务接口
public interface OrderService {
// 生成订单
void generate();
// 修改订单信息
void modify();
// 查看订单信息
void detail();
}
OrderServiceImpl实现类
package com.bjpowernode.dynamic.proxy;
public class OrderServiceImpl implements OrderService {
@Override
public void generate() {
try {
// 模拟订单生成耗时
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已生成...");
}
@Override
public void modify() {
try {
// 模拟修改订单耗时
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已修改...");
}
@Override
public void detail() {
try {
// 模拟查看订单耗时
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("正在查看详情...");
}
}
3.1 JDK动态代理
需求:业务增强,获取每个方法的执行时间,我们采用动态代理的方式,在内存中生成代理类!
客户端程序
第一步:还是创建目标对象。
第二步:创建代理对象,这个代理对象是动态生成的,此时需要Proxy类的newProxyInstance()方法,这个方法有三个参数:
①第一个参数:ClassLoader loader-类加载器,这个类加载器有什么用呢?
在内存当中生成的字节码也是class文件,要执行也得先加载到内存当中;加载类就需要类加载器,所以这里需要指定类加载器;并且JDK要求,目标类的类加载器必须和代理类的类加载器使用同一个。②第二个参数:Class<?>[] interfaces-代理类要实现的接口,代理类和目标类要实现同一个接口或一些接口;在内存中生成代理类的时候,这个代理类是需要告诉它实现哪些接口的
③第三个参数:InvocationHandler h-调用处理器;InvocationHandler 被翻译为:调用处理器,是一个接口;在调用处理器接口中编写的增强的业务代码。既然是接口,就要编写接口的实现类。
package com.bjpowernode.dynamic.client;
import com.bjpowernode.dynamic.proxy.OrderService;
import com.bjpowernode.dynamic.proxy.OrderServiceImpl;
import java.lang.reflect.Proxy;
public class Client {
public static void main(String[] args) {
// 创建目标对象
OrderService target = new OrderServiceImpl();
// 创建代理对象
Object obj = Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
"调用处理器");
// 调用代理对象的代理方法
// 返回的是一个Object,实现的是同一个OrderService接口,所以可以向下转型
OrderService proxyObj = (OrderService) obj;
proxyObj.generate();
proxyObj.modify();
proxyObj.detail();
}
}
第三步: 着重解决上面的第三个参数:调用处理器的参数,肯定要写一个实现类去实现InvocationHandler接口
(1)为什么强行要求你必须实现InvocationHandler接口?
因为一个类实现接口就必须实现接口中的方法;并且必须实现invoke()方法,因为JDK在底层调用invoke()方法的程序已经提前写好了!
注意:invoke()方法不是我们程序员负责调用的,是JDK负责调用的。
(2) invoke方法什么时候被调用呢?当代理对象调用代理方法的时候,注册在InvocationHandler调用处理器当中的invoke()方法被调用。
(3)invoke方法是JDK负责调用的,所以JDK调用这个方法的时候会自动给我们传过来这三个参数,我们可以在invoke方法的大括号中直接使用。invoke方法的三个参数:
①第一个参数:Object proxy 代理对象的引用,这个参数使用较少。
②第二个参数:Method method 目标对象上的目标方法。(要执行的目标方法就是它)
③第三个参数:Object[] args 目标方法上的实参。注意:invoke方法执行过程中,先使用TimeInvocationHandler(target)参数把目标对象传过来,然后在处理器中使用method参数通过反射机制来调用目标对象的目标方法;所以我们就需要目标对象,所以我们就提供了目标对象的属性,提供了构造方法用来赋值,根据前面TimeInvocationHandler(target)参数把传过来的目标对象进行赋值!
完整客户端程序
package com.bjpowernode.dynamic.client;
import com.bjpowernode.dynamic.proxy.OrderService;
import com.bjpowernode.dynamic.proxy.OrderServiceImpl;
import java.lang.reflect.Proxy;
public class Client {
public static void main(String[] args) {
// 创建目标对象
OrderService target = new OrderServiceImpl();
// 创建代理对象
Object obj = Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new TimeInvocationHandler(target));
// 调用代理对象的代理方法
// 返回的是一个Object,实现的是同一个OrderService接口,所以可以向下转型
OrderService proxyObj = (OrderService) obj;
// 调用方法是invoke方法里面的增强代码会执行,但是目标对象的目标方法并不会执行,
// 所以需要在new TimeInvocationHandler()中把target目标对象传过去
proxyObj.generate();
proxyObj.modify();
proxyObj.detail();
}
}
用来增强代码的处理器程序
package com.bjpowernode.dynamic.client;
import com.bjpowernode.dynamic.proxy.OrderService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class TimeInvocationHandler implements InvocationHandler {
// 目标对象
private Object target;
// 构造方法,给成员变量赋值
public TimeInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 增强代码
long begin = System.currentTimeMillis();
// 调用目标对象的目标方法(通过反射机制)
// 方法四要素:哪个对象target,哪个方法method,传什么参数args,返回什么值retValue
Object retValue = method.invoke(target, args);
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
return retValue;
}
}
细节:对于上面的代码一定要注意返回值的问题
现在我们的方法都是void没有返回值,看起来通过反射机制调用方法返回retValue没什么用;但是我们一旦提供一个有返回值的参数,如果不去return retValue,在客户端就获取不到这个值,获取到的就会是一个null!
封装一个工具类
package com.bjpowernode.dynamic.util;
import com.bjpowernode.dynamic.client.TimeInvocationHandler;
import java.lang.reflect.Proxy;
public class ProxyUtil {
// 封装一个工具方法,可以通过这个方法获取代理对象。
public static Object newProxyInstance(Object target){
// 底层是调用的还是JDK的动态代理。
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new TimeInvocationHandler(target));
}
}
使用封装后的工具类
3.2 CGLIB动态代理
CGLIB既可以代理接口,又可以代理类,底层采用继承的方式实现!
所以被代理的目标类不能使用final关键字修饰!
第一步:引入CGLIB的依赖
<!--引入CJLIB的依赖-->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
第二步:准备一个没有实现接口的类(可以代理类)
package com.bjpowernode.proxy.service;
public class UserService {
// 目标方法
public boolean login(String username, String password){
System.out.println("系统正在验证身份...");
if ("admin".equals(username) && "123".equals(password)) {
return true;
}
return false;
}
// 目标方法
public void logout(){
System.out.println("系统正在退出...");
}
}
第三步:客户端程序,使用CGLIB在内存中为UserService类生成代理类,并创建对象
①首先创建一个字节码增强器,创建Enhancer对象。
②因为cglib底层使用的是继承的方式实现,所以需要告诉cglib要继承哪个类,调用增强器的setSuperclass方法,参数是我们的要代理类的.class。
③调用增强器的setCallback方法,设置回调接口,参数是一个方法拦截器,这个拦截器需要我们自己编写一个类去实现MethodInterceptor接口。
④调用增强器的create方法,生成源码,编译class并加载到JVM,并创建代理对象。
⑤最后一步,调用代理对象的代理方法。
package com.bjpowernode.proxy.client;
import com.bjpowernode.proxy.service.TimerMethodInterceptor;
import com.bjpowernode.proxy.service.UserService;
import net.sf.cglib.proxy.Enhancer;
public class Client {
public static void main(String[] args) {
// 第一步:创建字节码增强器
Enhancer enhancer = new Enhancer();
// 第二步:告诉cjlib要继承那个类
enhancer.setSuperclass(UserService.class);
// 第三步:设置回调接口,参数是方法拦截器
enhancer.setCallback(new TimerMethodInterceptor());
// 第四步:生成源码,编译class,加载到JVM,并创建代理对象
UserService userServiceProxy = (UserService)enhancer.create();
// 第五步:调用方法
// 调用代理对象的代理方法。
boolean success = userServiceProxy.login("admin", "123");
System.out.println(success ? "登录成功" : "登录失败");
userServiceProxy.logout();
}
}
可以输出以下userServiceProxy(代理对象),记住格式,根据这个名字可以推测框架底层是否使用了CGLIB动态代理;其实底层是:
com.bjpowernode.proxy.service.UserService$$EnhancerByCGLIB$$4a68228b@2471cca7
这个类继承了userServiceProxy这个代理对象
第四步:着重解决第三步的回调函数,方法拦截器问题
和JDK动态代理原理差不多,在JDK动态dialing中需要提供的是InvocationHandler,而CGLIB动态代理提供的是:MethodInterceptor,所以也要编写一个方法实现这个接口,并重写MethodInterceptor接口中的intercept()方法,该方法有4个参数:
①第一个参数:目标对象;
②第二个参数:目标方法;
③第三个参数:目标方法调用时的实参;
④第四个参数:代理方法;
package com.bjpowernode.proxy.service;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class TimerMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
// 前增强
long begin = System.currentTimeMillis();
// 调用目标,通过代理对象的invokeSuper方法
Object retValue = methodProxy.invokeSuper(target, objects);
// 后增强
long end = System.currentTimeMillis();
System.out.println("耗时" + (end - begin) + "毫秒");
// 一定要返回
return retValue;
}
}
测试结果
这是因为cjlib底层的问题,JDK高于8的就会出现这个问题!怎么解决呢?设置两个参数!
①先点击Modify options
② 选择 Add VM options
③设置以下两个参数
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/sun.net.util=ALL-UNNAMED
④正常执行