代理模式 Proxy Pattern & Spring AOP 应用
结构型模式关注如何将现有类或对象组织一起形成更加强大的结构。
一、概述
代理模式(Proxy Pattern):给某个对象提供一个代理活占位符,并由代理对象来控制对原对象的访问。
二、结构
- Subject(抽象主题角色):
声明了真实主题和代理的主题的共同接口,这样一来,在任何使用真实主题的地方都可以使用代理主题,客户端通常需要针对抽象主题角色进行编程。 - Proxy(代理主题角色):
包含了对真实主题对象的引用,从而可以操作真实对象,在代理主题中提供了与真实主题相同的接口,对真实对象的使用加以控制并约束,可以在执行主题操作前后执行其他操作。 - RealSubject(真实主题角色):
定义了代理角色所代表的真实对象,在真实主题角色中实现了真正的业务操作,客户端可以通过代理主题角色调用真实角色中定义的操作
三、实现
我们以房屋租借问题中的房东、代理商为例:
抽象主题角色: IRent 租借接口:定义租借相关行为规范
/* IRent 租借接口:定义租借相关行为规范 */
public interface IRent {
void rent(); //租借
}
真实主题角色: HouseKeeping:房东类
/* HouseKeeping:房东类 */
public class HouseKeeping implements IRent {
/**
* 核心业务方法:租借房屋
*/
@Override
public void rent() {
System.out.println("我有一套房,租金1500每月");
}
}
1. 静态代理
代理主题角色: HouseAgency 租借代理商类
/* HouseAgency 租借代理商类 */
public class HouseAgency implements IRent {
//真实主题对象
private IRent rent;
/* 唯一构造器:初始化 */
public HouseAgency(IRent rent) {
this.rent = rent;
}
/**
* 核心业务方法:出租房屋
*/
@Override
public void rent() {
before();
Object result = method.invoke(this.real, args);
after();
}
/**
* 调用业务方法前进行处理的方法
*/
public void before() {
System.out.println("提前收押金");
}
/**
* 调用业务方法后进行处理的方法
*/
public void after() {
System.out.println("入住收物业费、水电费...");
}
}
测试代码:
使用 JUnit 单元测试:
@Test
public void test() {
/* 创建代理对象 */
IRent rent = new HouseAgency(new HouseKeeping());
/* 通过代理对象 */
rent.rent();
}
测试结果:
2. JDK 动态代理
从 JDK 1.3 开始,Java语言提供了对动态代理的支持,需要用到 java.lang.reflect
包下的一些类:
Proxy 类:
Proxy
类提供了用于创建动态代理类和实例对象的方法,它是创建动态代理类的父类,它最常用的方法如下:
/* 用于返回一个Class类型的代理类,并在参数中提供类加载器,并指定代理的接口数组 */
public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)
/* 用于返回一个动态创建的代理类实例:
第一个参数 loader 表示代理类的类加载器
第二个参数 interfaces 表示代理类所实现的接口列表
第三个参数 h 表示所指派的调用处理程序类
*/
public static Object newProxyInstance(ClassLoader loader, Class<?>... interfaces, InvocationHandler h)
InvocationHandler 接口:
代理处理程序的实现接口,该接口作为代理实例的调用处理者的公共父类,每一个代理类的实例都可以提供一个相关的具体调用处理者(InvocationHandler
接口的子类)
其核心方法如下:
/* 该方法用于代理对代理类实例的方法调用,代理实现业务方法被调用时,该方法自动被调用
第一个参数 proxy 表示代理类的实例
第二个参数 method 表示需要代理的方法
第三个参数 args 表示代理方法的参数
*/
public Object invork(Object proxy, Method method, Object[] args)
实现代码(代理主题角色):
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/* 代理主题角色:实现 InvocationHandler 接口 */
public class HouseProxy implements InvocationHandler {
//真实主题角色
private Object real;
/* 唯一构造器:初始化 */
public HouseProxy(Object real) {
this.real = real;
}
/**
* 产生代理对象
*/
public Object createProxy() {
return Proxy.newProxyInstance(this.real.getClass().getClassLoader(), this.real.getClass().getInterfaces(), this);
}
/**
* 代理对代理类实例的方法调用,代理实现业务方法被调用时,该方法自动被调用
* @param proxy 表示代理类的实例
* @param method 表示需要代理的方法
* @param args 表示代理方法的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(this.real, args);
after();
return result;
}
/**
* 调用业务方法前进行处理的方法
*/
public void before() {
System.out.println("提前收押金");
}
/**
* 调用业务方法后进行处理的方法
*/
public void after() {
System.out.println("入住收物业费、水电费...");
}
}
测试代码:
使用 JUnit 单元测试:
@Test
public void test(){
/* 产生代理对象 */
IRent rent = (IRent) new HouseProxy(new HouseKeeping()).createProxy();
/* 通过代理对象,调用业务方法 */
rent.rent();
}
测试结果:
3. CGLib代理
CGLib (Code Generation Library) ,一个强大的、高性能、高质量的 Code 生成类库。
它可以在运行期扩展 Java 类与实现 Java 接口。Hibernate 用它来实现 PO 字节码的动态生成。CGLib 比 Java 的 java.lang.reflect.Proxy 类更强的在于它不仅可以接管接口类的方法,还可以接管普通类的方法。CGlib继承要被动态代理的类,重写父类的方法,实现AOP面向切面编程。
JDK动态代理、CGLIB代理 区别:
- JDK动态代理只能对实现了接口的类生成代理,而不能针对类 。
- CGLIB针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法 。
两者速度对比:
- JDK动态代理是面向接口,在创建代理实现类时速度比CGLib快
- CGLib动态代理是通过字节码底层继承要代理类来实现(代理类不能被final关键字所修饰),运行速度比JDK动态代理更快
Maven 引入依赖:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
CGLib 的核心接口是位于 net.sf.cglib.proxy
包下的 MethodInterceptor
接口,它继承自 Callback
接口:
核心方法:
/** 代理对代理类实例的方法调用,代理实现业务方法被调用时,该方法自动被调用
* @param obj 表示本类
* @param method 表示需要代理的方法
* @param args 表示代理方法的参数
* @param proxy 代表对父类进行代理
*/
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable
实现代码:
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/* 代理主题角色:实现 MethodInterceptor 接口 */
public class CGLibProxy implements MethodInterceptor {
//真实主题角色
private Object real;
/* 唯一构造器:初始化 */
public CGLibProxy(Object real) {
this.real = real;
}
/**
* 创建代理对象
* @return
*/
public Object createProxy() {
Enhancer e = new Enhancer();
e.setSuperclass(this.real.getClass());
e.setCallback(this);
return e.create();
}
/** 代理对代理类实例的方法调用,代理实现业务方法被调用时,该方法自动被调用
* @param obj 表示本类
* @param method 表示需要代理的方法
* @param args 表示代理方法的参数
* @param proxy 代表对父类进行代理
*/
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
before();
Object result = proxy.invokeSuper(obj,args);
after();
return result;
}
/**
* 调用业务方法前进行处理的方法
*/
public void before() {
System.out.println("提前收押金");
}
/**
* 调用业务方法后进行处理的方法
*/
public void after() {
System.out.println("入住收物业费、水电费...");
}
}
测试代码:
@Test
public void test(){
/* 产生代理对象 */
HouseKeeping h = (HouseKeeping) new CGLibProxy(new HouseKeeping()).createProxy();
/* 通过代理对象,调用业务方法 */
h.rent();
}
测试结果:
四、特点
☯ 优点
- 能够协调调用者与被调用者,一定程度上降低了系统的耦合度
- 可针对抽象主题角色进行编程,更换代理类灵活、便于扩展,符合开闭原则
☯ 缺点
- 由于代理对象的出现,可能使得处理请求变慢
五、动态代理应用:AOP
AOP(Aspect-OrientedProgramming,面向切面编程),AOP包括切面(aspect)、通知(advice)、连接点(joinpoint),实现方式就是通过对目标对象的代理在连接点前后加入通知,完成统一的切面操作。
AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
AOP的应用技术:
- 采用动态代理技术,截获消息进行装饰,取代原有对象(真实主题角色)行为的执行
- 采用静态织入方式,在编译期间织入相关代码
六、Spring AOP
Spring 动态代理机制:
Spring 默认提供了两种方式来生成代理对象:JDK Proxy + CGLib,具体使用哪种根据情况而定,默认策略如下:
- 目标类是接口,使用 JDK动态代理
- 目标对象没有实现接口,采用 CGLIB代理
Spring 提供了配置参数来强制选择使用 CGLIB 技术,如下:
<aop:config proxy-target-class="true" />
CGLIB使用生成代理子类实现代理,proxy-target-class
表示属性值决定基于接口 / 类的代理被创建,proxy-target-class="true"
表示 强制使用 CGLIB 技术来实现AOP,若填入 <aop:config />
配置缺省,则依据 Spring 默认策略 选择代理。
相关术语:
- 通知(Advice):包含了需要用于多个应用对象的横切行为
- 连接点(Join Point):程序执行过程中能够应用通知的所有点
- 切点(PointCut):定义何时进行切入,哪些连接点会得到通知
- 切面(Aspect):通知、切点相结合
- 引入(Introduction):允许向现有类中添加新的属性、方法
- 织入(Weaving):将切面应用到目标对象,并创建新代理对象的过程,分为编译期织入、类加载期织入、运行期织入
Spring Boot 使用 AOP
在 SpringBoot 中使用 AOP 之前,首先要引入相关依赖:
使用 Maven 引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.5.4</version>
</dependency>
我们写一个小的demo,对AOP进行测试:
核心代码结构一览:
编写 Controller 并对其进行切面:
IDEA 对 AOP 支持度较高,被切面的方法,会在以特殊图标进行标记:
编写 Controller 代码:
package com.ljw.aop.controller; //包路径
//省略imports...
@RestController
@RequestMapping("/api/aop")
public class AopController {
@GetMapping("/hello")
public String hello(){
System.out.println("hello");
return "hello";
}
}
定义切点:
切点是通过@Pointcut注解和切点表达式定义的,@Pointcut注解可以在一个切面内定义可重用的切点。
Spring 切面力度最小可达到方法级别,使用 execution 表达式需致命方法返回类型、类名、方法名、参数名等相关信息,这种使用方式最为广泛:
定义通知:
五种通知类型(包括 IDEA 支持图标):
-
前置(@Before):目标方法调用前执行通知
-
后置(@After):目标方法调用后执行通知
-
环绕(@Around):目标方法调用前后执行通知
-
返回(@AfterReturning):目标方法成功执行之后执行通知
-
异常(@AfterThrowing):目标方法抛出异常之后执行通知
编写 Advice 代码:
package com.ljw.aop.advice; //包路径
//省略imports...
@Aspect
@Component
public class AopAdvice {
/**
* 定义切点
*/
@Pointcut("execution (* com.ljw.aop.controller.*.*(..))")
public void point() {
}
/**
* 前置通知
*/
@Before("point()")
public void before() {
System.out.println("before");
}
/**
* 后置通知
*/
@After("point()")
public void after() {
System.out.println("after");
}
/**
* 环绕通知
* @param proceedingJoinPoint 切入点
*/
@Around("point()")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
System.out.println("around-before"); //执行前
try {
proceedingJoinPoint.proceed(); //执行时
} catch (Throwable t) {
t.printStackTrace();
}
System.out.println("around-after"); //执行后
}
/**
* 方法执行完毕通知
*/
@AfterReturning("point()")
public void returnAdvice() {
System.out.println("finish");
}
/**
* 方法抛出异常
*/
@AfterThrowing("point()")
public void throwAdvice() {
System.out.println("Exception!!!");
}
}
访问路径:
访问成功:
访问后结果: