《SpringBoot 3:入门与应用实战》第 7 章 AOP 思想与实现 阅读笔记 13
在第 1 章中讲到过,Spring Framework 的两大核心是控制反转 (IOC) 与面向切面编程 (AOP),第 2~4 章了解了 IOC 相关的知识,接下来的第 7~8 章则是 AOP 部分的讲解。AOP 同样重要,它支撑了 Spring Framework 中众多扩展功能的运行,如声明式事务、注解式缓存等。与 IOC部分的讲解方式相同,先从 AOP 的思想演绎推理开始,了解 AOP 思想的由来。
7.1 AOP 是怎么来的
基于 spring-ioc-a 工程继续向下推演。
7.1.1 日志记录
项目运行了大半年之后,有一天客户突然给你打来了电话,表示系统内出现大量异常数据,需要协助排查和处理。
在系统的积分模块里,有几个用户的积分异常地多。但是在开发阶段,积分模块的权重不是很高,对于积分的操作没有很细致的流水记录,也就没办法追溯这些积分的来源。
当你跟客户解释事情的前因后果之后,客户表示是否可以把积分的变动过程都记录下来,这样仅凭客户自己也能追溯。起初你没有认为这件事情很麻烦,便一口答应。
代码预编写




为了演示积分变动的逻辑,为 DemoService 添加几个方法,代表积分变动的逻辑;相应地,为 DemoServiceImpl 添加对应的实现,完善代码后再复制一个相同的 DemoServiceImpl 并命名为 DemoServiceImpl2。

java
package com.yangjunbo.exampleb.service;
import java.util.List;
/**
* ClassName: DemoService
* Package: com.yangjunbo.service
* Description:
*
* @Author 杨钧博
* @Create 2026/8/11 13:20
* @Version 1.0
*/
public interface DemoService {
List<String> findAll();
int add(String userId, int points);
int subtract(String userId, int points);
int multiply(String userId, int points);
int divide(String userId, int points);
}

java
package com.yangjunbo.exampleb.service.impl;
import com.yangjunbo.exampleb.dao.DemoDao;
import com.yangjunbo.exampleb.factory.BeanFactory;
import com.yangjunbo.exampleb.service.DemoService;
import java.util.List;
/**
* ClassName: DemoServiceImpl
* Package: com.yangjunbo.service.impl
* Description:
*
* @Author 杨钧博
* @Create 2026/8/11 13:21
* @Version 1.0
*/
public class DemoServiceImpl implements DemoService {
//private DemoDao demoDao = new DemoDaoImpl();
//DemoDao demoDao = BeanFactory.getDemoDao();
DemoDao demoDao = (DemoDao) BeanFactory.getBean("demoDao");
@Override
public List<String> findAll() {
for (int i = 0; i < 5; i++) {
System.out.println(BeanFactory.getBean("demoDao"));
}
return demoDao.findAll();
}
@Override
public int add(String userId, int points) {
return points;
}
@Override
public int subtract(String userId, int points) {
return points;
}
@Override
public int multiply(String userId, int points) {
return points;
}
@Override
public int divide(String userId, int points) {
return points;
}
}
最后,在 DemoServlet 中添加对 DemoService 的 add 和 subtract 方法的调用。

java
package com.yangjunbo.exampleb.servlet;
import com.yangjunbo.exampleb.service.DemoService;
import com.yangjunbo.exampleb.service.impl.DemoServiceImpl;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(urlPatterns = "/demo2")
public class DemoServlet1 extends HttpServlet {
DemoService demoService = new DemoServiceImpl();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().println(demoService.findAll().toString());
demoService.add("bearbear", 666);
demoService.subtract("bearbear", 666);
}
}
至此,代码的前期准备完毕,下面继续进行演绎推导。
7.1.2 添加积分变动逻辑
仔细核查了一遍与积分模块相关的代码,发现事情并不像想象中的那样简单:涉及积分变动的逻辑非常多。虽然改动难度不大,但是改动数量很多,修改过程费时费力!
1.修改 DemoServiceImpl
既然是增加积分变动的日志记录,那么可以在每个方法执行开始时,将当前类的类名 + 执行的方法名以及参数都打印到控制台。

java
package com.yangjunbo.exampleb.service.impl;
import com.yangjunbo.exampleb.dao.DemoDao;
import com.yangjunbo.exampleb.factory.BeanFactory;
import com.yangjunbo.exampleb.service.DemoService;
import java.util.List;
/**
* ClassName: DemoServiceImpl
* Package: com.yangjunbo.service.impl
* Description:
*
* @Author 杨钧博
* @Create 2026/8/11 13:21
* @Version 1.0
*/
public class DemoServiceImpl implements DemoService {
//private DemoDao demoDao = new DemoDaoImpl();
//DemoDao demoDao = BeanFactory.getDemoDao();
DemoDao demoDao = (DemoDao) BeanFactory.getBean("demoDao");
@Override
public List<String> findAll() {
for (int i = 0; i < 5; i++) {
System.out.println(BeanFactory.getBean("demoDao"));
}
return demoDao.findAll();
}
@Override
public int add(String userId, int points) {
System.out.println("DemoServiceImpl add ...");
System.out.println("user: " + userId + ", points: " + points);
return points;
}
@Override
public int subtract(String userId, int points) {
System.out.println("DemoServiceImpl add ...");
System.out.println("user: " + userId + ", points: " + points);
return points;
}
@Override
public int multiply(String userId, int points) {
System.out.println("DemoServiceImpl add ...");
System.out.println("user: " + userId + ", points: " + points);
return points;
}
@Override
public int divide(String userId, int points) {
System.out.println("DemoServiceImpl add ...");
System.out.println("user: " + userId + ", points: " + points);
return points;
}
}
虽然代码编写起来很简单,但由于每个方法都要覆盖到位,一旦改动量大起来,会导致你在改动过程中异常痛苦。在改代码的过程当中,逐渐产生了一种想法:这些日志打印的逻辑几乎是一样的,最起码可以封装一个工具类,通过调用工具类的方法打印日志。
2.封装 LogUtils
快速实现后形成了一个最基本的日志打印工具类 LogUtils。

java
package com.yangjunbo.exampleb.utils;
public class LogUtils {
public static void printLog(String className, String methodName, String userId, int points) {
System.out.println(className + " " + methodName + " ...");
System.out.println("user: " + userId + ", points: " + points);
}
}
很明显,工具类不具备通用性,如果能让所有的业务逻辑都可以使用该工具类,则参数列表就不能如此固定,于是稍加改造后就有了通用工具类。

java
package com.yangjunbo.exampleb.utils;
import java.util.Arrays;
//public class LogUtils {
// public static void printLog(String className, String methodName, String userId, int points) {
// System.out.println(className + " " + methodName + " ...");
// System.out.println("user: " + userId + ", points: " + points);
// }
//}
public class LogUtils {
public static void printLog(String className, String methodName, Object... args) {
System.out.println(className + " " + methodName + " ...");
System.out.println("参数列表: " + Arrays.toString(args));
}
}
有了 LogUtils 工具类后,DemoServiceImpl 中的代码就显得稍微精简一些。

java
package com.yangjunbo.exampleb.service.impl;
import com.yangjunbo.exampleb.dao.DemoDao;
import com.yangjunbo.exampleb.factory.BeanFactory;
import com.yangjunbo.exampleb.service.DemoService;
import com.yangjunbo.exampleb.utils.LogUtils;
import java.util.List;
/**
* ClassName: DemoServiceImpl
* Package: com.yangjunbo.service.impl
* Description:
*
* @Author 杨钧博
* @Create 2026/8/11 13:21
* @Version 1.0
*/
public class DemoServiceImpl implements DemoService {
//private DemoDao demoDao = new DemoDaoImpl();
//DemoDao demoDao = BeanFactory.getDemoDao();
DemoDao demoDao = (DemoDao) BeanFactory.getBean("demoDao");
@Override
public List<String> findAll() {
for (int i = 0; i < 5; i++) {
System.out.println(BeanFactory.getBean("demoDao"));
}
return demoDao.findAll();
}
@Override
public int add(String userId, int points) {
LogUtils.printLog("DemoServiceImpl", "add", userId, points);
return points;
}
@Override
public int subtract(String userId, int points) {
LogUtils.printLog("DemoServiceImpl", "add", userId, points);
return points;
}
@Override
public int multiply(String userId, int points) {
LogUtils.printLog("DemoServiceImpl", "add", userId, points);
return points;
}
@Override
public int divide(String userId, int points) {
LogUtils.printLog("DemoServiceImpl", "add", userId, points);
return points;
}
}
虽然代码量变少了,但仅仅少了一点,对于每个方法还是要写 LogUtils.printLog 方法,相当于没改!如果将现在 DemoServiceImpl 类中的方法罗列开来,则此时 4 个方法的结构如图所示。

所以,有没有什么方法,能让这个日志打印的代码从核心 Service 层中移除,但同样实现打印日志的效果?核心业务逻辑跟这些附加的动作混合在一起,会导致代码臃肿,不利于维护。
7.1.3 引入设计模式
在 GoF 23 设计模式中,有一些设计模式能部分解决当前的问题。
1.引入装饰者模式
在行为型模式中,装饰者模式可以基于原有的逻辑扩展额外的动作,从表面上看装饰者模式可以解决当前问题。所以我们可以尝试着这样改造一下原有的代码。通过声明一个装饰者,使其实现与被装饰者同样的接口,并重写这些方法,从而达成方法增强的目的。

java
package com.yangjunbo.exampleb.service.impl.decorator;
import com.yangjunbo.exampleb.service.DemoService;
import com.yangjunbo.exampleb.utils.LogUtils;
import java.util.List;
// 装饰者实现与被装饰者同样的接口
public class DemoServiceDecorator implements DemoService {
private DemoService target;
// 构造方法中需要传入被装饰的原对象
public DemoServiceDecorator(DemoService target) {
this.target = target;
}
@Override
public List<String> findAll() {
return target.findAll();
}
@Override
public int add(String userId, int points) {
// 在原对象执行方法之前打印日志,完成日志与业务逻辑的分离
LogUtils.printLog("DemoService", "add", userId, points);
return target.add(userId, points);
}
@Override
public int subtract(String userId, int points) {
LogUtils.printLog("DemoService", "subtract", userId, points);
return target.subtract(userId, points);
}
@Override
public int multiply(String userId, int points) {
LogUtils.printLog("DemoServiceImpl", "add", userId, points);
return target.multiply(userId, points);
}
@Override
public int divide(String userId, int points) {
LogUtils.printLog("DemoServiceImpl", "add", userId, points);
return target.divide(userId, points);
}
}
经过如此包装后,DemoServlet 再获取 DemoService 的时候就可以利用 DemoServiceDecorator 包装,形成增强的装饰对象。而且利用装饰者模式的特性,可以对原对象反复装饰。换言之,DemoService 可以增强的逻辑不仅仅是日志记录,如果有事务也可以加入事务控制,如果有其他校验等逻辑也可以编写装饰者来包装。

java
package com.yangjunbo.exampleb.servlet;
import com.yangjunbo.exampleb.factory.BeanFactory;
import com.yangjunbo.exampleb.service.DemoService;
import com.yangjunbo.exampleb.service.impl.decorator.DemoServiceDecorator;
import com.yangjunbo.exampleb.service.impl.decorator.DemoServiceImpl;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(urlPatterns = "/demo2")
public class DemoServlet2 extends HttpServlet {
//DemoService demoService = new DemoServiceImpl();
DemoService demoService = new DemoServiceDecorator(new DemoServiceImpl());
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().println(demoService.findAll().toString());
demoService.add("bearbear", 666);
demoService.subtract("bearbear", 666);
}
}
看上去这个方案可行,实际使用的效果如何?当我们落实编码时,才发现装饰者的弊端:对于每个业务层接口都要写一个装饰者!现在只有积分变动逻辑需要添加日志记录,如果后续用户修改密码也需要添加日志记录,那么 UserService 也需要编写装饰者;部门发生变动也需要添加日志记录,那么 DepartmentService 也需要编写装饰者,由此产生的编码量未免过大。
2.引入模板方法模式
行为型模式中另一个可以抽取逻辑的模式是模板方法模式。如果使用模板方法模式来改造代码,可以增加一个 AbstractDemoService 类来抽取出日志的打印逻辑。

java
package com.yangjunbo.exampleb.service.impl.model;
import com.yangjunbo.exampleb.service.DemoService;
import com.yangjunbo.exampleb.utils.LogUtils;
public abstract class AbstractDemoService implements DemoService {
@Override
public int add(String userId, int points) {
// 父类执行额外的逻辑
LogUtils.printLog("DemoService", "add", userId, points);
return doAdd(userId, points);
}
// 子类负责业务功能实现
protected abstract int doAdd(String userId, int points);
@Override
public int subtract(String userId, int points) {
// 父类执行额外的逻辑
LogUtils.printLog("DemoService", "subtract", userId, points);
return doAdd(userId, points);
}
// 子类负责业务功能实现
protected abstract int doSubtract(String userId, int points);
@Override
public int multiply(String userId, int points) {
// 父类执行额外的逻辑
LogUtils.printLog("DemoService", "mutiply", userId, points);
return doAdd(userId, points);
}
// 子类负责业务功能实现
protected abstract int doMultiply(String userId, int points);
@Override
public int divide(String userId, int points) {
// 父类执行额外的逻辑
LogUtils.printLog("DemoService", "divide", userId, points);
return doAdd(userId, points);
}
// 子类负责业务功能实现
protected abstract int doDivide(String userId, int points);
}
如此抽取之后问题也可以被解决,DemoServiceImpl 就不再需要实现 DemoService 接口的 add 方法,而是只需要实现 AbstractDemoService 提供的 doAdd 方法。

java
package com.yangjunbo.exampleb.service.impl.model;
import com.yangjunbo.exampleb.dao.DemoDao;
import com.yangjunbo.exampleb.factory.BeanFactory;
import com.yangjunbo.exampleb.service.DemoService;
import java.util.List;
/**
* ClassName: DemoServiceImpl
* Package: com.yangjunbo.service.impl
* Description:
*
* @Author 杨钧博
* @Create 2026/8/11 13:21
* @Version 1.0
*/
public class DemoServiceImpl extends AbstractDemoService {
//private DemoDao demoDao = new DemoDaoImpl();
//DemoDao demoDao = BeanFactory.getDemoDao();
DemoDao demoDao = (DemoDao) BeanFactory.getBean("demoDao");
@Override
public List<String> findAll() {
for (int i = 0; i < 5; i++) {
System.out.println(BeanFactory.getBean("demoDao"));
}
return demoDao.findAll();
}
@Override
public int doAdd(String userId, int points) {
return points;
}
@Override
public int doSubtract(String userId, int points) {
return points;
}
@Override
public int doMultiply(String userId, int points) {
return points;
}
@Override
public int doDivide(String userId, int points) {
return points;
}
}
然而到这里,读者是否意识到代码走向的问题,使用模板方法模式编写代码,总体的代码量比装饰者模式多,而且如此编码还有一个很棘手的点:由于模板方法模式使用了继承,一个 DemoService 只能扩展一个功能,无法同时实现日志记录、事务等复合逻辑增强,因此这种方式的灵活性甚至不如装饰者模式。
3.引入责任链模式
行为型模式中还有一种可以抽取逻辑的模式,那就是责任链模式。责任链模式最大的特点是将一个动作的请求放在一条对象链上传播,直到责任链上的某个对象能处理该请求时为止。针对多种不同功能的扩展,这种设计模式似乎不像责任链,而更像装饰者,所以我们可以这样理解,对于功能的扩展,装饰者跟责任链能实现的最终效果几乎一致。但是由前面的推演可知,用装饰者模式所要编写的代码已经多出很多,从代码规模上讲本身就不是一个好的方案,所以责任链模式也不适合。
4.代理模式
通过前面 3 个行为型模式的推演,下面引出真正的解决方案:代理模式。从代理的生成方式来看,代理分为静态代理和动态代理:静态代理需要自行编写代理类,组合原有的目标对象,并实现原有目标对象实现的接口,以此来完成对原有对象方法功能的增强;动态代理只需要编写增强逻辑类,在运行时动态将增强逻辑类组合进原有的目标对象,即可生成代理对象,完成对目标对象方法功能的增强。
在实际编写中,有读者可能会产生一种错觉:代理和装饰者的编写几乎一样,没有清晰地感受到二者的不同。对这个问题用一句话解释为,代理模式侧重的是对原有目标对象的访问权限控制,而装饰者模式是在原有对象之上增强功能。
这样看起来似乎用装饰者模式更适合完成这个功能,但这里面存在一个小问题:Java 语言中没有动态装饰者,所以我们使用动态代理来解决该问题。
5.OOP 的不足与横切的思想
从上面几个设计模式的尝试和分析中,发现了面向对象编程 (OOP) 的一个不足之处:诸如上述这种相同、重复的逻辑,OOP 没有办法将这些逻辑分离出去,OOP 只能尽可能地减少这些重复的代码,却无法避免重复代码的出现。
再观察一下上面的代码和图,很明显可以发现,四个方法中的起始动作都是日志打印的方法,它们可以用一个横截的矩形框起来,如图所示。

这种矩形框可以是一个类的几个方法,也可以是多个类的不同方法。只要这些方法的开始/结束都有相同的逻辑,就可以把这些逻辑都提取出来视为一体,这个思想就叫横切,提取出来的逻辑所组成的虚拟结构,可以称为切面(上图的矩形框就可以理解为一个切面)。
7.1.4 使用动态代理
下面引入动态代理机制。Java 早在 JDK 1.3 中就引入了动态代理,具体的用法以及 Cglib 的动态代理,会在 7.2 节中进行复习和回顾,这里我们先实操并观察效果。
1.DemoServlet 中使用动态代理
既然是 Servlet 要依赖 DemoService,那么可以借助 Servlet 的生命周期,在 Servlet 初始化时从 BeanFactory 中获取 DemoService,然后借助JDK 动态代理生成 DemoService 的代理对象,并给其中的方法进行增强。编写完成后就可以重新部署应用,重启 Tomcat 测试。

java
package com.yangjunbo.exampleb.servlet;
import com.yangjunbo.exampleb.factory.BeanFactory;
import com.yangjunbo.exampleb.service.DemoService;
import com.yangjunbo.exampleb.service.impl.proxy.DemoServiceImpl;
import com.yangjunbo.exampleb.utils.LogUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Proxy;
@WebServlet(urlPatterns = "/demo3")
public class DemoServlet3 extends HttpServlet {
DemoService demoService;
@Override
public void init() throws ServletException {
DemoService demoService = new DemoServiceImpl();
Class<? extends DemoService> clazz = demoService.getClass();
// 使用JDK动态代理,生成代理对象
this.demoService = (DemoService) Proxy
.newProxyInstance(clazz.getClassLoader(),
clazz.getInterfaces(), (proxy, method, args) -> {
LogUtils.printLog("DemoService", method.getName(), args);
return method.invoke(demoService, args);
});
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().println(demoService.findAll().toString());
demoService.add("bearbear", 666);
demoService.subtract("bearbear", 666);
}
}

java
package com.yangjunbo.exampleb.service.impl.proxy;
import com.yangjunbo.exampleb.dao.DemoDao;
import com.yangjunbo.exampleb.factory.BeanFactory;
import com.yangjunbo.exampleb.service.DemoService;
import java.util.List;
/**
* ClassName: DemoServiceImpl
* Package: com.yangjunbo.service.impl
* Description:
*
* @Author 杨钧博
* @Create 2026/8/11 13:21
* @Version 1.0
*/
public class DemoServiceImpl implements DemoService {
//private DemoDao demoDao = new DemoDaoImpl();
//DemoDao demoDao = BeanFactory.getDemoDao();
DemoDao demoDao = (DemoDao) BeanFactory.getBean("demoDao");
@Override
public List<String> findAll() {
for (int i = 0; i < 5; i++) {
System.out.println(BeanFactory.getBean("demoDao"));
}
return demoDao.findAll();
}
@Override
public int add(String userId, int points) {
return points;
}
@Override
public int subtract(String userId, int points) {
return points;
}
@Override
public int multiply(String userId, int points) {
return points;
}
@Override
public int divide(String userId, int points) {
return points;
}
}
当 Tomcat 启动完成后,借助浏览器访问 /demo3,观察控制台的打印内容如下。可以发现 add 方法和 subtract 方法有打印日志,这个结果是正确的,但是 findAll 方法的调用也伴随着日志打印,这个结果不是我们预期的。出现该结果的原因很容易想到,因为 JDK 的动态代理原本就是给原有对象的所有方法都进行增强,打印结果与我们一开始的需求不符,我们的预期是只给 add、subtract 等积分变动的方法增强,所以要对增强逻辑进行修改。


2.过滤方法
解决方案很简单,在 DemoService 的代理对象创建中,对构造的 InvocationHandler 内部逻辑添加方法名称的判断就可以实现。

java
package com.yangjunbo.exampleb.servlet;
import com.yangjunbo.exampleb.service.DemoService;
import com.yangjunbo.exampleb.service.impl.proxy.DemoServiceImpl;
import com.yangjunbo.exampleb.utils.LogUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.List;
@WebServlet(urlPatterns = "/demo4")
public class DemoServlet4 extends HttpServlet {
DemoService demoService;
@Override
public void init() throws ServletException {
DemoService demoService = new DemoServiceImpl();
Class<? extends DemoService> clazz = demoService.getClass();
this.demoService = (DemoService) Proxy.newProxyInstance(clazz.getClassLoader(),
clazz.getInterfaces(), (proxy, method, args) -> {
List<String> list = Arrays.asList("add", "subtract", "multiply", "divide");
if (list.contains(method.getName())) {
LogUtils.printLog("DemoService", method.getName(), args);
}
return method.invoke(demoService, args);
});
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().println(demoService.findAll().toString());
demoService.add("bearbear", 666);
demoService.subtract("bearbear", 666);
}
}
注意,上述代码中又出现了软件开发中常见的 "坏习惯":硬编码!很明显方法名称的过滤可以分离到外部化配置文件中!所以下面在src/main/resources/exampleb 目录下新建一个 proxy.properties 文件,在这个文件中定义日志打印的增强代理方法。

log.methods=add,subtract,multiply,divide
相应地,由 Servlet 负责加载该配置文件,并创建 DemoService 的代理对象。

java
package com.yangjunbo.exampleb.servlet;
import com.yangjunbo.exampleb.service.DemoService;
import com.yangjunbo.exampleb.service.impl.proxy.DemoServiceImpl;
import com.yangjunbo.exampleb.utils.LogUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
@WebServlet(urlPatterns = "/demo5")
public class DemoServlet5 extends HttpServlet {
DemoService demoService;
@Override
public void init() throws ServletException {
// 读取proxy.properties
Properties proxyProp = new Properties();
try {
proxyProp.load(this.getClass().getClassLoader()
.getResourceAsStream("exampleb/proxy.properties"));
} catch (IOException e) {
throw new ExceptionInInitializerError("DemoServlet11 initialize error, cause: " + e.getMessage());
}
DemoService demoService = new DemoServiceImpl();
Class<? extends DemoService> clazz = demoService.getClass();
this.demoService = (DemoService) Proxy.newProxyInstance(clazz.getClassLoader(),
clazz.getInterfaces(), (proxy, method, args) -> {
// 从配置文件中取出要增强的方法名
List<String> list = Arrays.asList(proxyProp.getProperty("log.methods").split(","));
if (list.contains(method.getName())) {
LogUtils.printLog("DemoService", method.getName(), args);
}
return method.invoke(demoService, args);
});
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().println(demoService.findAll().toString());
demoService.add("bearbear", 666);
demoService.subtract("bearbear", 666);
}
}
重新启动 Tomcat,并访问 /demo5 路径,可以发现这次只有 add 和 subtract 方法的执行中打印了日志,符合我们对程序运行的预期。

7.1.5 代理对象的创建者
上面的代码编写中,读者是否会产生一个疑问:代理对象是否应当由 Servlet 创建?如果有很多 Servlet 依赖了相同的 Service,那岂不是要重复创建多次,每个 Servlet 都要去创建 Service 的代理对象。由此可见,Servlet 应该直接获取的 Service 被代理过的对象,而不是原始的 Service对象。所以上述的推演仍不是最佳方案。
1.重新设计 properties 文件
在 properties 文件中定义了 demoService 和增强类的全限定名,也声明了这个增强类要增强的方法列表,这样 BeanFactory 加载到 properties 文件后就能取得这两个信息。

demoDao=com.yangjunbo.exampleb.dao.impl.DemoOracleDaoImpl
demoService=com.yangjunbo.exampleb.service.impl.proxy.DemoServiceImpl
demoService.proxy.class=com.yangjunbo.exampleb.service.impl.proxy.LogAdvisor
demoService.proxy.methods=add,subtract,multiply,divide
2.编写 LogAdvisor
既然改造的方案是将增强的 InvocationHandler 提到外部单独实现,就必须定义一个新的类 LogAdvisor。LogAdvisor 的 invoke 方法中需要取得被代理的原始对象,所以需要使用成员属性 + 构造方法的方式,传入原来的被代理对象;另外考虑到增强方法的过滤效果,所以相应地还要把methods 也一并传入。

java
package com.yangjunbo.exampleb.service.impl.proxy;
import com.yangjunbo.exampleb.utils.LogUtils;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
public class LogAdvisor implements InvocationHandler {
private Object target;
private List<String> methods;
public LogAdvisor(Object target, String[] methods) {
this.target = target;
this.methods = Arrays.asList(methods);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (this.methods.contains(method.getName())) {
LogUtils.printLog(target.getClass().getName(), method.getName(), args);
}
return method.invoke(target, args);
}
}
3.修改 BeanFactory
最后修改 BeanFactory,从需求描述中可以了解,要想在调用 getBean 方法时创建代理对象,就需要在 getBean 的内部进行一些逻辑扩展。
要使用动态代理生成代理对象,那么在创建 bean 对象后,先不要着急放入 beanMap,而需要检索 factory.properties 文件中是否定义了 proxy相关的属性。按照约定,可以通过获取当前 bean 对象的名称并添加后缀 ".proxy.class",判断该属性是否有对应的值。如果获取的proxyAdvisorClassName 不为空,则代表这个 bean 对象定义过代理增强,需要反射创建 InvocationHandler 的实现类,并从 properties 文件中获取该 bean 对象需要增强(被代理)的方法列表,即后缀为 ".proxy.methods" 的配置属性。

java
package com.yangjunbo.exampleb.factory;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
public class BeanFactory {
// public static DemoDao getDemoDao() {
// // return new DemoMysqlDaoImpl();
// return new DemoOracleDaoImpl();
// }
// public static DemoDao getDemoDao() {
// try {
// return (DemoDao) Class.forName("com.yangjunbo.dao.impl.DemoOracleDaoImpl")
// .getDeclaredConstructor().newInstance();
// } catch (Exception e) {
// e.printStackTrace();
// throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
// }
// }
private static Properties properties;
// 使用静态代码块初始化properties,加载factory.properties文件
static {
properties = new Properties();
try {
// 必须使用类加载器读取resource文件夹下的配置文件
properties.load(BeanFactory.class.getClassLoader()
.getResourceAsStream("exampleb/factory.properties"));
} catch (IOException e) {
// BeanFactory类的静态初始化失败,后续代码也没有必要继续执行,抛出异常
throw new ExceptionInInitializerError("BeanFactory initialize error, cause: " + e.getMessage());
}
}
// public static DemoDao getDemoDao() {
// try {
// Class<?> beanClazz = Class.forName(properties.getProperty("demoDao"));
// return (DemoDao) beanClazz.getDeclaredConstructor().newInstance();
// }catch (Exception e){
// throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
// }
// }
// public static Object getBean(String beanName) {
// try {
// // 从properties文件中读取指定name对应类的全限定名,并反射实例化
// Class<?> beanClazz = Class.forName(properties.getProperty(beanName));
// return beanClazz.getDeclaredConstructor().newInstance();
// }catch (Exception e){
// throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
// }
// }
// 缓存区,保存已经创建好的对象
private static Map<String, Object> beanMap = new HashMap<>();
public static Object getBean(String beanName) {
// 双检锁保证beanMap中确实没有beanName对应的对象
if (!beanMap.containsKey(beanName)) {
synchronized (BeanFactory.class) {
if (!beanMap.containsKey(beanName)) {
// 过了双检锁,证明确实没有,可以执行反射创建
try {
Class<?> beanClazz = Class.forName(properties.getProperty(beanName));
Object bean = beanClazz.newInstance();
// 检查properties中是否定义了代理增强
String proxyAdvisorClassName = properties.getProperty(beanName + ".proxy.class");
if (proxyAdvisorClassName != null && proxyAdvisorClassName.trim().length() > 0) {
Class<?> proxyAdvisorClass = Class.forName(proxyAdvisorClassName);
String[] methods = properties.getProperty(beanName + ".proxy.methods").split(",");
}
beanMap.put(beanName, bean);
}catch (Exception e){
throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
}
}
}
}
return beanMap.get(beanName);
}
}
有了 InvocationHandler 的实现类类型和增强方法,下面就可以创建代理对象。前面定义过 LogAdvisor 的构造方法,这里一定可以获取一个两参数的构造方法,因此在 BeanFactory 中就可以直接声明构造方法需要传入的参数列表,从而反射创建 LogAdvisor 的对象。

java
package com.yangjunbo.exampleb.factory;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
public class BeanFactory {
// public static DemoDao getDemoDao() {
// // return new DemoMysqlDaoImpl();
// return new DemoOracleDaoImpl();
// }
// public static DemoDao getDemoDao() {
// try {
// return (DemoDao) Class.forName("com.yangjunbo.dao.impl.DemoOracleDaoImpl")
// .getDeclaredConstructor().newInstance();
// } catch (Exception e) {
// e.printStackTrace();
// throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
// }
// }
private static Properties properties;
// 使用静态代码块初始化properties,加载factory.properties文件
static {
properties = new Properties();
try {
// 必须使用类加载器读取resource文件夹下的配置文件
properties.load(BeanFactory.class.getClassLoader()
.getResourceAsStream("exampleb/factory.properties"));
} catch (IOException e) {
// BeanFactory类的静态初始化失败,后续代码也没有必要继续执行,抛出异常
throw new ExceptionInInitializerError("BeanFactory initialize error, cause: " + e.getMessage());
}
}
// public static DemoDao getDemoDao() {
// try {
// Class<?> beanClazz = Class.forName(properties.getProperty("demoDao"));
// return (DemoDao) beanClazz.getDeclaredConstructor().newInstance();
// }catch (Exception e){
// throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
// }
// }
// public static Object getBean(String beanName) {
// try {
// // 从properties文件中读取指定name对应类的全限定名,并反射实例化
// Class<?> beanClazz = Class.forName(properties.getProperty(beanName));
// return beanClazz.getDeclaredConstructor().newInstance();
// }catch (Exception e){
// throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
// }
// }
// 缓存区,保存已经创建好的对象
private static Map<String, Object> beanMap = new HashMap<>();
public static Object getBean(String beanName) {
// 双检锁保证beanMap中确实没有beanName对应的对象
if (!beanMap.containsKey(beanName)) {
synchronized (BeanFactory.class) {
if (!beanMap.containsKey(beanName)) {
// 过了双检锁,证明确实没有,可以执行反射创建
try {
Class<?> beanClazz = Class.forName(properties.getProperty(beanName));
Object bean = beanClazz.newInstance();
// 检查properties中是否定义了代理增强
String proxyAdvisorClassName = properties.getProperty(beanName + ".proxy.class");
if (proxyAdvisorClassName != null && proxyAdvisorClassName.trim().length() > 0) {
Class<?> proxyAdvisorClass = Class.forName(proxyAdvisorClassName);
String[] methods = properties.getProperty(beanName + ".proxy.methods").split(",");
// 要求InvocationHandler的实现类必须声明两参数构造方法
// 其中第一个参数是被代理的目标对象,第二个参数是要增强的方法列表
InvocationHandler proxyHandler = (InvocationHandler) proxyAdvisorClass
.getConstructors()[0].newInstance(bean, methods);
}
beanMap.put(beanName, bean);
}catch (Exception e){
throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
}
}
}
}
return beanMap.get(beanName);
}
}
如此创建后,原始对象、InvocationHandler 都已经准备就绪,接下来可以使用动态代理创建代理对象。

java
package com.yangjunbo.exampleb.factory;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
public class BeanFactory {
// public static DemoDao getDemoDao() {
// // return new DemoMysqlDaoImpl();
// return new DemoOracleDaoImpl();
// }
// public static DemoDao getDemoDao() {
// try {
// return (DemoDao) Class.forName("com.yangjunbo.dao.impl.DemoOracleDaoImpl")
// .getDeclaredConstructor().newInstance();
// } catch (Exception e) {
// e.printStackTrace();
// throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
// }
// }
private static Properties properties;
// 使用静态代码块初始化properties,加载factory.properties文件
static {
properties = new Properties();
try {
// 必须使用类加载器读取resource文件夹下的配置文件
properties.load(BeanFactory.class.getClassLoader()
.getResourceAsStream("exampleb/factory.properties"));
} catch (IOException e) {
// BeanFactory类的静态初始化失败,后续代码也没有必要继续执行,抛出异常
throw new ExceptionInInitializerError("BeanFactory initialize error, cause: " + e.getMessage());
}
}
// public static DemoDao getDemoDao() {
// try {
// Class<?> beanClazz = Class.forName(properties.getProperty("demoDao"));
// return (DemoDao) beanClazz.getDeclaredConstructor().newInstance();
// }catch (Exception e){
// throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
// }
// }
// public static Object getBean(String beanName) {
// try {
// // 从properties文件中读取指定name对应类的全限定名,并反射实例化
// Class<?> beanClazz = Class.forName(properties.getProperty(beanName));
// return beanClazz.getDeclaredConstructor().newInstance();
// }catch (Exception e){
// throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
// }
// }
// 缓存区,保存已经创建好的对象
private static Map<String, Object> beanMap = new HashMap<>();
public static Object getBean(String beanName) {
// 双检锁保证beanMap中确实没有beanName对应的对象
if (!beanMap.containsKey(beanName)) {
synchronized (BeanFactory.class) {
if (!beanMap.containsKey(beanName)) {
// 过了双检锁,证明确实没有,可以执行反射创建
try {
Class<?> beanClazz = Class.forName(properties.getProperty(beanName));
Object bean = beanClazz.newInstance();
// 检查properties中是否定义了代理增强
String proxyAdvisorClassName = properties.getProperty(beanName + ".proxy.class");
if (proxyAdvisorClassName != null && proxyAdvisorClassName.trim().length() > 0) {
Class<?> proxyAdvisorClass = Class.forName(proxyAdvisorClassName);
String[] methods = properties.getProperty(beanName + ".proxy.methods").split(",");
// 要求InvocationHandler的实现类必须声明两参数构造方法
// 其中第一个参数是被代理的目标对象,第二个参数是要增强的方法列表
InvocationHandler proxyHandler = (InvocationHandler) proxyAdvisorClass
.getConstructors()[0].newInstance(bean, methods);
// 动态代理创建对象
Object proxy = Proxy.newProxyInstance(bean.getClass().getClassLoader(),
bean.getClass().getInterfaces(), proxyHandler);
bean = proxy;
// 经过该步骤后,放入beanMap的对象就是已经被增强过的代理对象
}
beanMap.put(beanName, bean);
}catch (Exception e){
throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
}
}
}
}
return beanMap.get(beanName);
}
}
4.修改 Servlet
从 BeanFactory 获取 Service 的代理对象。

java
package com.yangjunbo.exampleb.servlet;
import com.yangjunbo.exampleb.factory.BeanFactory;
import com.yangjunbo.exampleb.service.DemoService;
import com.yangjunbo.exampleb.service.impl.proxy.DemoServiceImpl;
import com.yangjunbo.exampleb.utils.LogUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
@WebServlet(urlPatterns = "/demo6")
public class DemoServlet6 extends HttpServlet {
DemoService demoService = (DemoService) BeanFactory.getBean("demoService");
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().println(demoService.findAll().toString());
demoService.add("bearbear", 666);
demoService.subtract("bearbear", 666);
}
}
5.测试运行
重启 Tomcat,使用浏览器访问 Servlet 的匹配路径 /demo6,发现控制台依旧能正常打印日志,证明 Servlet 取得的 DemoService 是代理对象,演绎推理完毕。

6.小结
AOP 思想的场景演绎完毕,跟 IOC 思想的引入一样,先总结一下整个过程中出现的几个关键点。
- 使用传统的 GoF 23 设计模式可以在一定程度上解决重复代码的问题,但是没法完全把扩展逻辑代码和核心业务逻辑代码分开。
- 使用动态代理可以在不改变原有逻辑的前提下,对已有方法进行增强。
- 创建代理对象应由 IOC 容器负责,而不是由使用者负责。
7.1.6 引入 AOP 思想
最后引出本节的主题。图中展示了 AOP 横切的思想,其中的矩形框被称为切面,英文表示为 Aspect,它表示的是分布在一个或多个类中多个方法内的相同逻辑。利用动态代理将这部分相同的逻辑抽取为一个独立的 Advisor 增强器,并在原始对象的初始化过程中动态产生代理对象,完成增强功能。在此基础上,通过指定增强的类名、方法名(甚至方法参数、列表类型等),可以更细粒度地对方法进行增强。使用这种方式,可以在不修改原始代码的前提下,通过抽取相同的扩展逻辑,对已有功能代码进行增强。这就是所谓的面向切面编程 (Aspect Oriented Programming,AOP)。
