设计模式–代理模式
什么是代理模式
在代理模式(Proxy Pattern)即一个类代表另一个类的功能。属于设计模式中的结构型模式。
在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。
代理模式的作用
代理模式的作用在于某些情况下我们不方便或不希望直接访问某个对象,或者希望在使用某对象之前之后增加一些扩展的处理时,可以为目标对象创建一个代理对象,使用代理对象来完成并扩展目标对象的功能。
使用代理对象可以在不修改目标的情况下完成了功能的扩展,这也符合设计原则中的开闭原则,即:对修改关闭,对扩展开放。
举个栗子:老王接手了他人遗留下来的项目有如下代码:
@Service
public class NumServiceImpl implements NumService {
@Override
public int getRundomNum(int start, int end) {
Random r = new Random();
return r.nextInt(end - start) + start;
}
//.......
}
处理其他业务的时候老王想要使用这个类,但是发现这个方法并不完善,它没有做参数的校验,也就是说当参数start > end的时候会抛错,于是老王根据业务场景做了如下修改:
@Service
public class NumServiceImpl implements NumService {
@Override
public int getRundomNum(int start, int end) {
if (start >= end) {
return -1;
}
Random r = new Random();
return r.nextInt(end - start) + start;
}
//.......
}
一切似乎非常顺利,接下来就可以使用这个方法了。但是发布运行之后项目的另一个功能却出现了问题,查看代码之后发现此方法有一处调用:
//......
try {
int res = numService.getRundomNum(userInput1, userInput2);
return "结果是"+res;
}catch (Exception e){
return "您的输入有误哦";
}
//.......
通过上述案例我们可以看出:对于已经存在的代码,直接修改需要十分谨慎,稍有不慎就会影响其他的调用逻辑。
当然,上述案例中的目标代码只是一个非常简单的案例,我们可以简单的复制出另一个简单的方法 , 或者直接在调用之前进行判断。但是,在某些场景下或许会由于 目标代码十分复杂、扩展功能比较复杂或者目标代码属于引用的第三方jar 又该怎么处理呢?
这里就可以使用代理模式,创建一个代理对象,通过代理对象来对目标进行扩展,并不修改目标的代码,这样也就不会影响到目标涉及的其他业务
Java中实现代理模式的方式
Java中实现代理模式主要有三种方式:
- 静态代理
- 动态代理
- cglib代理
静态代理
静态代理 , 顾名思义这是一种固定的代理方式,通常由Coder编写或者特定程序生成源代码然后编译运行,也就是说动态代理这种方式所需要的的代理对象类的class文件是在程序运行之前就已经存在的,在使用的时候也是创建这个类的实例即可。
对于上述提到的例子,用静态带的方式可以做如下处理:
创建代理类:
@Component
public class NumServiceProxy implements NumService{
@Autowired
NumServiceImpl numService;
@Override
public int getRundomNum(int start, int end) {
if (start>=end){
return -1;
}
return numService.getRundomNum(start, end);
}
}
然后使用代理类执行
@Autowired
@Qualifier("numServiceProxy")
NumService numService;
@Test
public void m1() {
int rundomNum = numService.getRundomNum(9,8);
System.out.println(rundomNum);
}
动态代理
静态代理能够在不修改目标代码的情况下生成代理对象,但是缺点也很明显:
- 需要为每一个扩展的类维护一个代理类
- 必须继承同一个接口或抽象类
- 扩展仍然需要目标类的对象
Java 1.3以后,Java提供了动态代理技术,允许开发者在运行期创建接口的代理实例。
动态代理在框架中经常被使用到,比如MyBatis框架就是通过解析配置文件或扫描注解得到创建代理类的规则,然后在运行期间为数据层接口创建代理对象的。Spring的AOP功能也是在运行期间对标注需要切入的类生成代理对象来进行增强的。
动态代理有两种实现方式:jdk动态代理和cglib代理
Jkd动态代理
Jdk动态代理是java提供的类库,不需要依赖第三方类库即可实现。Jdk动态代理通过Java提供的Proxy的静态方法newProxyInstance 来创建代理类对象, 代理类的生成实在运行期间进行的。
先搂一眼方法定义:
/**
* Returns an instance of a proxy class for the specified interfaces that dispatches method invocations to the specified invocation handler.
* ... ...
* @param loader the class loader to define the proxy class 定义代理类的类加载器
* @param interfaces the list of interfaces for the proxy class to implement
* 代理类要实现的接口列表
* @param h the invocation handler to dispatch method invocations to
*
* ... ...
**/
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
newProxyInstance
方法需要一个类加载器、代理类需要实现的接口列表、以及一个InvocationHandler
类型的参数 :
/**
* Processes a method invocation on a proxy instance and returns
* the result. This method will be invoked on an invocation handler
* when a method is invoked on a proxy instance that it is
* associated with.
* 处理代理实例上的方法调用并返回结果。 当在与之关联的代理实例上调用方法时,将在调用处理程序上调用此方法。
**/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
jdk动态代理可以直接为接口生成一个代理对象:
1.定义一个注解用来指定数字计算的方法,1 2 3 4 分别代表加减乘除操作
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = ElementType.METHOD)
public @interface Operation {
/**
* 1234 +-*\
*/
int value() default 1;
}
2.定义NumService接口和几个抽象方法 分别标注注解
public interface NumService {
@Operation(1)
int addNum(int num1, int num2);
@Operation(2)
int subtractNum(int num1, int num2);
@Operation(3)
int multiplyNum(int num1, int num2);
@Operation(4)
int divideNum(int num1, int num2);
}
3.接下来在测试程序中通过动态代理生成NumService的代理类对象,代理类对象中的方法逻辑根据注解来动态生成
public void test() {
NumService numService = (NumService)Proxy.newProxyInstance(
NumService.class.getClassLoader(),
new Class[]{NumService.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Operation annotation = method.getAnnotation(Operation.class);
int arg0 = (int) args[0];
int arg1 = (int) args[1];
switch (annotation.value()) {
case 1:
return arg0 + arg1;
case 2:
return arg0 - arg1;
case 3:
return arg0 * arg1;
case 4:
return arg0 / arg1;
default:
break;
}
return null;
}
}
);
同时我们用动态代理方案页可以对目标实现类进行代理,比如文章开头提到问题可以使用动态代理做如下解决方案:
@Configuration
public class BeanConfig {
@Autowired
NumInvokeHandle numInvokeHandle;
@Bean
public NumService numProxyService(){
return (NumService) Proxy.newProxyInstance(NumService.class.getClassLoader(), new Class[]{NumService.class}, numInvokeHandle);
}
}
@Component
public class NumInvokeHandle implements InvocationHandler {
@Autowired
NumServiceImpl numServiceImpl;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//扩展的前置参数校验
try {
int arg1 = (int) args[0];
int arg2 = (int) args[1];
if (arg1 < arg2) {
return -1;
}
} catch (Exception e) {
e.printStackTrace();
}
//执行目标对象的方法
return method.invoke(numServiceImpl, args);
}
}
@Autowired
@Qualifier("numProxyService")
NumService numService;
@Test
public void m2() {
int rundomNum = numService.getRundomNum(9,8);
System.out.println(rundomNum);
}
cgilb代理
jdk动态代理解决了静待代理必须依赖目标实现类,和必须预先生成class文件的问题,但是这种方案仍然是在实现相同接口的前提下才能实现的,可以说jdk动态代理其实是对目标接口的代理。
但是在某种情况下如果目标类并没有使用面向接口编程的思想来进行编码,jdk动态代理就无法使用。
CGLib采用底层的字节码技术,全称是:Code Generation Library,CGLib可以为一个类创建一个子类,在子类中采用方法拦截的技术拦截所有父类方法的调用并顺势织入横切逻辑。
先说使用方法,用cglib动态代理的方法处理文章开头提到的问题:
- cglib动态代理需要引入cglib的依赖
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>
- 配置生成代理对象并放到容器
@Configuration public class BeanConfig { @Bean public NumService numProxyService(){ Enhancer enhancer = new Enhancer(); //设置要代理的类 enhancer.setSuperclass(NumServiceImpl.class); //设置回调 enhancer.setCallback(new MethodInterceptor(){ @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { Object invoke = methodProxy.invokeSuper(o, objects); return invoke; } }); return (NumService) enhancer.create(); } }
- 测试
@Autowired @Qualifier("numProxyService") NumService numService; @Test public void m2() { int rundomNum = numService.getRundomNum(9,8); System.out.println(rundomNum); }