zl程序教程

您现在的位置是:首页 >  后端

当前栏目

【Java 设计模式 · 结构型 & AOP】代理模式(Proxy Pattern)& Spring AOP 应用

2023-09-27 14:22:51 时间

结构型模式关注如何将现有类或对象组织一起形成更加强大的结构。

一、概述

代理模式(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的应用技术:

  1. 采用动态代理技术,截获消息进行装饰,取代原有对象(真实主题角色)行为的执行
  2. 采用静态织入方式,在编译期间织入相关代码

六、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!!!");
    }
}

访问路径:
访问
访问成功:
访问成功
访问后结果:
访问后控制台输出结果