Spring AOP入门-AOP是怎么来的

从 0 开始深入学习 Spring 专栏目录总览

从这一章开始,咱就算开始学习 AOP 了哈。跟 IOC 部分一样,我们还是先来了解一下 AOP 的演变。

上次在 IOC 部分我们用过的 Servlet 三层架构的工程还都有吧,我们在这上面继续做演变。

1. 【需求】日志记录

继 BeanFactory 抽取完成之后,你负责的项目最终是稳定的在客户的服务器上运行,客户给予了你高度的赞赏,一期的项目也就告一段落了。项目运行了大半年之后,有一天客户突然给你打来了电话:

靓仔啦,我们这边不知道咋回事,系统里出现了好多异常数据的啦,你有时间来帮我们看一下的啦 ~

本来这会你也不忙,就开了远程软件连上服务器看了一下,果不其然,在这个系统的积分模块里,有几个用户的积分异于常人的多。但是吧,系统里一开始就没把这个积分当做很重要的模块来设计,所以也没有什么积分的流水记录,也就没办法查这些积分的来源了。

你跟客户说明这个事情之后,客户显得比较着急:这不大行啊,积分也是很重要滴,能不能帮我们把积分的变动都记录下来啊,这样我们回头查起来也方便一些。起初你也没觉得这事多麻烦,就一口答应了。

代码预编写

为迎合接下来的剧情变动,我们需要做一些准备工作。

首先,复制 GitHub 中 spring-00-introduction 工程的 e_cachedfactory 目录,完整的拷贝一份出来到 f_pointslog :( Servlet 记得改名)

Spring AOP入门-AOP是怎么来的

接下来,resources 目录下的 factory_e.properties 也拷贝一份,命名为 factory_f.properties ,记得把里面的类名同步的改成 f 包的:

demoService=com.linkedbear.architecture.f_pointslog.service.impl.DemoServiceImpl
demoDao=com.linkedbear.architecture.f_pointslog.dao.impl.DemoDaoImpl

还有 BeanFactory 中的 properties 加载路径改掉:

static {
    properties = new Properties();
    try {
        // 记得改这里
        properties.load(BeanFactory.class.getClassLoader().getResourceAsStream("factory_f.properties"));
    } catch (IOException e) {
        throw new ExceptionInInitializerError("BeanFactory initialize error, cause: " + e.getMessage());
    }
}

然后,给 DemoService 添加几个方法,代表积分变动的逻辑:

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);
}

相应的,给 DemoServiceImpl 中添加对应的实现:(这里我们就不再拿着 DemoDao 折腾了,怪费劲的 … )

@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;
}

再复制一个相同的 DemoServiceImpl ,命名为 DemoServiceImpl2 ,代码不需要变。

最后,在 DemoServlet6 中,添加对 add 和 subtract 方法的调用:

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    resp.getWriter().println(demoService.findAll().toString());
    demoService.add("bearbear", 666);
    demoService.subtract("bearbear", 666);
}

到这里,代码的前期准备就搞定了,下面咱继续来讲故事。

2. 【初修改】积分变动逻辑添加

答应完事之后,你缓缓的打开了原来的工程,仔细搜查了一遍之后,发现事情不简单了:涉及到积分变动的逻辑好多啊!虽然改起来难度不大,但这么多改起来也挺费劲啊!

2.1 修改DemoServiceImpl

既然是加日志记录,那就在每个方法执行的一开始,先把这个类的类名 + 执行的方法名,以及参数都打印出来吧:

@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 subtract ...");
    System.out.println("user: " + userId + ", points: " + points);
    return points;
}

// 省略multiply与divide......

这代码写的,你是一边写一边骂啊,得亏这业务方法不多,要是来上个百儿八十个,那我不又得跟之前那帮孙子改数据库似的?

可是。。。聪明的你在写的时候,越来越感觉这事不对劲:哎这不对劲啊,这些日志的打印,它们的逻辑几乎都是一样的诶!我干嘛非得一个一个的这样写呢?最起码的,我封装个工具类也好啊!

2.2 封装LogUtils

于是,你的脑海中出现了这样的一个工具类:

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));
    }
}

这样改完之后,DemoServiceImpl 中就变成了这样子:

@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", "subtract", userId, points);
    return points;
}

// 省略multiply与divide......

好歹的少了一行代码吧,最起码复制粘贴起来能轻快点。。。

可是。。。也仅仅是少了一点吧,每个方法还是得写 LogUtils.printLog 方法,相当于没改!

Spring AOP入门-AOP是怎么来的

所以,有没有什么方法,能让这个日志打印的代码从核心 Service 中移除掉,但同样实现这个效果呢?(毕竟,当 Service 一旦大量增加之后,核心 Service 的逻辑会跟这些附加的动作混为一起,实在太过臃肿,代码会变得很难维护)

3. 引入设计模式

在 GoF23 设计模式中,其实有一些设计模式,是能解决一部分当下的这个问题的。(我知道很多小伙伴们都在想着代理模式了,不过小册想在此之前先讲点别的,顺便让小伙伴们加深一下以下引入的几个设计模式的对比)

3.1 【尝试】引入装饰者模式?

在行为型模式中,装饰者模式就可以给原有的逻辑上扩展额外的动作,看样子装饰者是可以解决这个问题的!所以我们可以尝试着这样改造一下原有的代码:

// 装饰者实现被装饰者同样的接口
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);
    }
    
    // 省略multiply与divide......
}

经过这样一包装后,在 DemoServlet 再获取 DemoService 的时候,就可以用 DemoServiceDecorator 包装一下了:

@WebServlet(urlPatterns = "/demo8")
public class DemoServlet8 extends HttpServlet {
    
    DemoService demoService = new DemoServiceDecorator((DemoService) BeanFactory.getBean("demoService"));

而且利用装饰者模式的特性,可以对原对象反复装饰!换言之, DemoService 可以增强的逻辑不仅仅是日志记录了,如果有事务也可以加入事务控制,如果有其它校验等逻辑也可以编写装饰者来包装它!

这个主意不错,实行一下试试?

3.2 【问题】装饰者的弊端

想法虽好,可到了编码实行的时候,才发现装饰者的一个最大的弊端:**每个业务层接口都要写一个装饰者!**现在只有积分变动逻辑需要添加日志记录,回头用户修改密码也要加日志记录,那 UserService 也要写装饰者;部门发生变动也要加日志记录,那 DepartmentService 也就需要写装饰者,这编码量未免也太大了吧!

不行,装饰者模式解决这个问题不是上乘之选,那还有没有更简单的办法了呢?

3.3 【再次尝试】引入模板方法模式?

行为型模式中还有一个可以抽取逻辑的模式,那就是模板方法模式。在之前 IOC 部分的学习中,我们也看到了,SpringFramework 中大量运用了模板方法模式来控制底层的逻辑结构。那如果用模板方法模式来改造代码的话,我们倒是可以加一个 AbstractDemoService 类来抽取出日志的打印逻辑:

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);
    
    // 省略其余方法......
}

这样的抽取倒是也可以解决问题啦,DemoServiceImpl 就不再需要实现 DemoService 接口的 add 方法,而是只需要实现 doAdd 方法即可:

public class DemoServiceImpl extends AbstractDemoService implements DemoService {
    
    DemoDao demoDao = (DemoDao) BeanFactory.getBean("demoDao");
    
    @Override
    public List<String> findAll() {
        return demoDao.findAll();
    }
    
    // 只需要实现父类留下的doXXX方法即可
    @Override
    protected int doAdd(String userId, int points) {
        return points;
    }
    
    // 省略其余方法......
}

3.4 【问题】模板方法模式的弊端

但是。。。写到这里小伙伴们是不是感觉怪怪的。。。怎么感觉这样写下来,代码量比装饰者模式更多了???而且,还有一个很棘手的点:由于模板方法模式使用了继承,一个 DemoService 只能扩展一个功能了!这灵活性还不如装饰者模式呢!

不行,模板方法模式也解决不了这个问题,还有别的办法吗?

3.5 【再次尝试】引入责任链模式?

行为型模式中还有一种可以抽取逻辑的模式,那就是责任链模式。责任链最大的特点是将一个动作的请求放在一条对象的链子上传播,直到职责链上的某个对象能处理该请求时终止。用这种设计模式的话,针对多种不同功能的扩展,似乎这不像是责任链,更像是装饰者。。。所以我们可以这样理解,对于功能的扩展,装饰者跟责任链能实现的最终效果是几乎一致的。

但是。。。(又但是了),前面我们已经看过了,装饰者模式要编写的代码已经好多了,能不能省省了。。。少写点,真的好累哦!

3.6 【方案】代理模式

好了,前面那么多铺垫,就是为了引出代理模式。从代理的生成方式来看,代理分为静态代理和动态代理:静态代理需要自行编写代理类,组合原有的目标对象,并实现原有目标对象实现的接口,以此来做到对原有对象的方法功能的增强;动态代理只需要编写增强逻辑类,在运行时动态将增强逻辑类组合进原有的目标对象,即可生成代理对象,完成对目标对象的方法功能增强。

有关静态代理的内容,小册在此处不作讲解,小伙伴们可以自己动手写一写。

在实际编写中,可能会有小伙伴产生一种错觉:代理和装饰者的编写几乎一样啊,它俩有什么不同呢?是这样,代理模式侧重的是对原有目标对象的访问权限控制,而装饰者是在原有对象之上增强功能。

这样看起来似乎装饰者更适合完成这个功能,但这里面存在一个小问题:没有动态装饰者。。。(笑哭)所以我们就使用动态代理来尝试解决该问题了。

3.7 【思想】OOP的不足&横切的思想

从上面的几个设计模式的尝试和分析中,我们发现了一个 OOP 的很大的不足之处:诸如上面的这种相同、重复的逻辑,OOP 没有办法将这些逻辑分离出去,OOP 只能尽可能的减少这些重复的代码量,却无法避免重复代码的出现

再观察一下上面的代码和示意图,我们很明显可以发现,四个方法中的起始动作都是日志打印的方法,它们可以用一个横截的框 框起来:

Spring AOP入门-AOP是怎么来的

这种框可以是一个类的几个方法,可以是多个类的不同方法。只要这些方法的开始 / 结束都有相同的逻辑,那我们就可以把这些逻辑都拿出来视为一体,这个思想就叫横切,提取出来的逻辑组成的虚拟的结构,我们可以称之为横切面(上图的红框就可以理解为一个横切面)。

4. 最终方案-动态代理

好了终于可以引出动态代理了,Java 早在 jdk 1.3 中就引入动态代理了,具体的用法,以及 Cglib 的动态代理,咱下一章会作一个复习和回顾,这里咱先写一下看看效果。

既然是 Servlet 要依赖 DemoService ,那我们可以先这样,让 Servlet 在初始化的时候,从 BeanFactory 中获取 DemoService ,然后借助 jdk 动态代理生成 DemoService 的代理对象,并给其中的方法增强:

@WebServlet(urlPatterns = "/demo10")
public class DemoServlet10 extends HttpServlet {
    
    DemoService demoService;
    
    @Override
    public void init() throws ServletException {
        DemoService demoService = (DemoService) BeanFactory.getBean("demoService");
        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);
                });
    }
    // ......
}

然后,按照之前在 IOC 入门中的方法,部署好应用。

4.1 【问题】方法全部被增强

Tomcat 启动完成后,咱直接访问 /demo10 ,观察控制台的打印:

DemoService findAll ...
参数列表: null
DemoService add ...
参数列表: [bearbear, 666]
DemoService subtract ...
参数列表: [bearbear, 666]

add 方法和 subtract 方法有打印日志,这个一点毛病也没有,但是 findAll 怎么也打印了呢?很简单,jdk 的动态代理,本来就是给原有对象的所有方法都进行增强

但我们一开始的需求,是只给 add 、subtract 等积分变动的方法增强,这个要怎么办呢?

4.2 【方案】过滤方法

解决方案倒是很简单,在 DemoService 的代理对象创建逻辑中,添加方法名称的判断就 OK 了:

@Override
public void init() throws ServletException {
    DemoService demoService = (DemoService) BeanFactory.getBean("demoService");
    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);
            });
}

等一下,先不要着急重启,看看这 “糟糕” 的代码,想想有什么问题?

又是硬编码了啊!这些东西很明显可以分离到外部化配置中!所以我们可以在 resources 目录下再创建一个 proxy.properties ,在这个文件中定义日志打印的增强代理方法:

log.methods=add,subtract,multiply,divide

然后,由 Servlet 负责加载该配置文件,并创建 DemoService 的代理对象:

@Override
public void init() throws ServletException {
    // 读取proxy.properties
    Properties proxyProp = new Properties();
    try {
        proxyProp.load(this.getClass().getClassLoader().getResourceAsStream("proxy.properties"));
    } catch (IOException e) {
        throw new ExceptionInInitializerError("DemoServlet11 initialize error, cause: " + e.getMessage());
    }

    DemoService demoService = (DemoService) BeanFactory.getBean("demoService");
    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);
            });
}

重新启动 Tomcat ,刷新,可以发现这次只有 add 和 subtract 方法的执行中打印了日志:

DemoService add ...
参数列表: [bearbear, 666]
DemoService subtract ...
参数列表: [bearbear, 666]

到这里,基本上 AOP 的概念就快出来了,不过小伙伴先不要着急,咱继续往下推演。

5. 【问题】代理对象的创建者?

上面的代码编写中,小伙伴是否有一种感觉:代理对象应该由 Servlet 创建吗?那回头如果有好多个 Servlet 都依赖了这个 Service ,那岂不是要重复创建好多次?

很明显,Servlet 要拿到的 Service 应该是被代理过的吧!那这个问题应该如何解决呢?

当然是由 BeanFactory 帮忙创建啦!按道理讲,如果引入了代理的机制,那么 BeanFactory 创建的对象就应该是被增强过的代理对象!好,明确了需求之后,下面我们来试着改造一下原有的 BeanFactory 。

5.1 重新设计properties

之前的 factory.properties 中,我们只定义过 bean 的名称对应的类的全限定名:

demoService=com.linkedbear.architecture.l_proxyfactory.service.impl.DemoServiceImpl
demoDao=com.linkedbear.architecture.l_proxyfactory.dao.impl.DemoDaoImpl

这次加入代理的增强后,这些很明显就不够了,我们可以试着这样写一下代理的声明:

demoService=com.linkedbear.architecture.l_proxyfactory.service.impl.DemoServiceImpl
demoService.proxy.class=com.linkedbear.architecture.l_proxyfactory.proxy.LogAdvisor
demoService.proxy.methods=add,subtract,multiply,divide

demoDao=com.linkedbear.architecture.l_proxyfactory.dao.impl.DemoDaoImpl

看,我在 properties 中额外定义了 demoService 的增强类的全限定名,也声明了这个增强类要增强的方法列表,这样 BeanFactory 加载到 properties 中就能拿到这两个信息。

5.2 编写LogAdvisor

既然这次要把增强的 InvocationHandler 拿到外面单独实现,那就必须要编写新的类了,咱新建一个 proxy 包,把这个 LogAdvisor 放在这里。(这个类的命名也是一个伏笔啊)

public class LogAdvisor implements InvocationHandler {
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(???, args);
    }
}

等一下,这个 method 的 invoke 方法中,不是要拿被代理的对象吗?可是这里还没有呢,那我们用成员属性 + 构造器的方式,把原来的被代理对象传进来:

public class LogAdvisor implements InvocationHandler {
    
    private Object target;
    
    public LogAdvisor(Object target) {
        this.target = target;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(target, args);
    }
}

这样就做到全方法的增强了,不过咱前面还封装了 methods 呢(不要忘记需求哦),所以相应的,咱把 methods 也引入进来:

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);
    }
}

这样 LogAdvisor 的逻辑就写完了,相对还是比较简单的哈。

为了保持风格统一,在本章中如果编写其他的 Advisor ,也必须是像上面一样,声明一个两参数的构造方法。

5.3 修改BeanFactory

最后咱把 BeanFactory 修改一下,前面的需求中咱说了,要在 getBean 的时候把代理对象创建出来,所以此处要做一些逻辑的扩展了。

    Class<?> beanClazz = Class.forName(properties.getProperty(beanName));
    Object bean = beanClazz.newInstance();
    beanMap.put(beanName, bean);

这是原来的 getBean 方法中,通过了双检锁后真正创建 bean 对象的逻辑,这里咱要扩展了。

要使用动态代理生成代理对象,那么在创建完 bean 后,先不要着急放入 beanMap ,去看一看 properties 中是否有定义 proxy 相关的属性:

    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) {

    }
    
    beanMap.put(beanName, bean);

可见,如果获取到的 proxyAdvisorClassName 不为空,则代表这个 bean 有定义代理增强,需要反射创建 InvocationHandler 的实现类。

接下来,反射到 InvocationHandler 的实现类后,还要从 properties 中获取这个 bean 要增强(被代理)的方法列表:

    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);

这样写完之后,就可以创建对象了吧!由于前面我们在 LogAdvisor 中有定义过编码风格,所以这里一定可以获取到一个构造方法,而且是两个参数的:

    public LogAdvisor(Object target, String[] methods) {
        this.target = target;
        this.methods = Arrays.asList(methods);
    }

所以我们在 BeanFactory 中就可以这样反射创建 LogAdvisor 的对象:

    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);

经过这样的创建之后,原始对象、InvocationHandler 都有了,接下来就可以使用动态代理,创建代理对象了:

    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) {
        // 有定义代理增强,需要反射创建InvocationHandler的实现类
        Class<?> proxyAdvisorClass = Class.forName(proxyAdvisorClassName);
        
        // 从properties中找出当前bean需要增强的方法列表
        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);

5.4 还原Servlet

最后,把 Servlet 的逻辑改回之前的样子:

@WebServlet(urlPatterns = "/demo12")
public class DemoServlet12 extends HttpServlet {
    
    DemoService demoService = (DemoService) BeanFactory.getBean("demoService");
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().println(demoService.findAll().toString());
        demoService.add("bearbear", 666);
        demoService.subtract("bearbear", 666);
    }
}

5.5 测试运行

重启 Tomcat ,重新访问 Servlet 的路径,发现控制台依旧能正常打印日志:

com.linkedbear.architecture.l_proxyfactory.service.impl.DemoServiceImpl add ...
参数列表: [bearbear, 666]
com.linkedbear.architecture.l_proxyfactory.service.impl.DemoServiceImpl subtract ...
参数列表: [bearbear, 666]

证明 Servlet 拿到的 DemoService 已经是代理对象了。


到这里,整个场景也都演绎完毕了,跟 IOC 的引入一样,咱先总结一下整个过程中出现的几个关键点:

  • 传统的 GoF23 设计模式中可以一定程度上解决重复代码,但整体上编码量会激增
  • 使用动态代理可以在不改变原有逻辑的前提下,对已有方法增强
  • 创建代理对象应由 IOC 容器负责,而不是使用者本身

6. AOP的思想引入【重点】

最后,引出本章的主题。

Spring AOP入门-AOP是怎么来的

这个图中的红框我们说它称为横切面,英文表示为 Aspect ,它表示的是分布在一个 / 多个类的多个方法中的相同逻辑。利用动态代理,将这部分相同的逻辑抽取为一个独立的 Advisor 增强器,并在原始对象的初始化过程中,动态组合原始对象并产生代理对象,同样能完成一样的功能增强。在此基础上,通过指定增强的类名、方法名(甚至方法参数列表类型等),可以更细粒度的对方法增强。使用这种方式,可以在不修改原始代码的前提下,对已有任意代码的功能增强。而这种针对相同逻辑的扩展和抽取,就是所谓的面向切面编程(Aspect Oriented Programming,AOP)

从这一章开始,咱就算开始学习 AOP 了哈。跟 IOC 部分一样,我们还是先来了解一下 AOP 的演变。

上次在 IOC 部分我们用过的 Servlet 三层架构的工程还都有吧,我们在这上面继续做演变。

1. 【需求】日志记录

继 BeanFactory 抽取完成之后,你负责的项目最终是稳定的在客户的服务器上运行,客户给予了你高度的赞赏,一期的项目也就告一段落了。项目运行了大半年之后,有一天客户突然给你打来了电话:

靓仔啦,我们这边不知道咋回事,系统里出现了好多异常数据的啦,你有时间来帮我们看一下的啦 ~

本来这会你也不忙,就开了远程软件连上服务器看了一下,果不其然,在这个系统的积分模块里,有几个用户的积分异于常人的多。但是吧,系统里一开始就没把这个积分当做很重要的模块来设计,所以也没有什么积分的流水记录,也就没办法查这些积分的来源了。

你跟客户说明这个事情之后,客户显得比较着急:这不大行啊,积分也是很重要滴,能不能帮我们把积分的变动都记录下来啊,这样我们回头查起来也方便一些。起初你也没觉得这事多麻烦,就一口答应了。

代码预编写

为迎合接下来的剧情变动,我们需要做一些准备工作。

首先,复制 GitHub 中 spring-00-introduction 工程的 e_cachedfactory 目录,完整的拷贝一份出来到 f_pointslog :( Servlet 记得改名)

Spring AOP入门-AOP是怎么来的

接下来,resources 目录下的 factory_e.properties 也拷贝一份,命名为 factory_f.properties ,记得把里面的类名同步的改成 f 包的:

demoService=com.linkedbear.architecture.f_pointslog.service.impl.DemoServiceImpl
demoDao=com.linkedbear.architecture.f_pointslog.dao.impl.DemoDaoImpl

还有 BeanFactory 中的 properties 加载路径改掉:

static {
    properties = new Properties();
    try {
        // 记得改这里
        properties.load(BeanFactory.class.getClassLoader().getResourceAsStream("factory_f.properties"));
    } catch (IOException e) {
        throw new ExceptionInInitializerError("BeanFactory initialize error, cause: " + e.getMessage());
    }
}

然后,给 DemoService 添加几个方法,代表积分变动的逻辑:

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);
}

相应的,给 DemoServiceImpl 中添加对应的实现:(这里我们就不再拿着 DemoDao 折腾了,怪费劲的 … )

@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;
}

再复制一个相同的 DemoServiceImpl ,命名为 DemoServiceImpl2 ,代码不需要变。

最后,在 DemoServlet6 中,添加对 add 和 subtract 方法的调用:

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    resp.getWriter().println(demoService.findAll().toString());
    demoService.add("bearbear", 666);
    demoService.subtract("bearbear", 666);
}

到这里,代码的前期准备就搞定了,下面咱继续来讲故事。

2. 【初修改】积分变动逻辑添加

答应完事之后,你缓缓的打开了原来的工程,仔细搜查了一遍之后,发现事情不简单了:涉及到积分变动的逻辑好多啊!虽然改起来难度不大,但这么多改起来也挺费劲啊!

2.1 修改DemoServiceImpl

既然是加日志记录,那就在每个方法执行的一开始,先把这个类的类名 + 执行的方法名,以及参数都打印出来吧:

@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 subtract ...");
    System.out.println("user: " + userId + ", points: " + points);
    return points;
}

// 省略multiply与divide......

这代码写的,你是一边写一边骂啊,得亏这业务方法不多,要是来上个百儿八十个,那我不又得跟之前那帮孙子改数据库似的?

可是。。。聪明的你在写的时候,越来越感觉这事不对劲:哎这不对劲啊,这些日志的打印,它们的逻辑几乎都是一样的诶!我干嘛非得一个一个的这样写呢?最起码的,我封装个工具类也好啊!

2.2 封装LogUtils

于是,你的脑海中出现了这样的一个工具类:

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));
    }
}

这样改完之后,DemoServiceImpl 中就变成了这样子:

@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", "subtract", userId, points);
    return points;
}

// 省略multiply与divide......

好歹的少了一行代码吧,最起码复制粘贴起来能轻快点。。。

可是。。。也仅仅是少了一点吧,每个方法还是得写 LogUtils.printLog 方法,相当于没改!

Spring AOP入门-AOP是怎么来的

所以,有没有什么方法,能让这个日志打印的代码从核心 Service 中移除掉,但同样实现这个效果呢?(毕竟,当 Service 一旦大量增加之后,核心 Service 的逻辑会跟这些附加的动作混为一起,实在太过臃肿,代码会变得很难维护)

3. 引入设计模式

在 GoF23 设计模式中,其实有一些设计模式,是能解决一部分当下的这个问题的。(我知道很多小伙伴们都在想着代理模式了,不过小册想在此之前先讲点别的,顺便让小伙伴们加深一下以下引入的几个设计模式的对比)

3.1 【尝试】引入装饰者模式?

在行为型模式中,装饰者模式就可以给原有的逻辑上扩展额外的动作,看样子装饰者是可以解决这个问题的!所以我们可以尝试着这样改造一下原有的代码:

// 装饰者实现被装饰者同样的接口
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);
    }
    
    // 省略multiply与divide......
}

经过这样一包装后,在 DemoServlet 再获取 DemoService 的时候,就可以用 DemoServiceDecorator 包装一下了:

@WebServlet(urlPatterns = "/demo8")
public class DemoServlet8 extends HttpServlet {
    
    DemoService demoService = new DemoServiceDecorator((DemoService) BeanFactory.getBean("demoService"));

而且利用装饰者模式的特性,可以对原对象反复装饰!换言之, DemoService 可以增强的逻辑不仅仅是日志记录了,如果有事务也可以加入事务控制,如果有其它校验等逻辑也可以编写装饰者来包装它!

这个主意不错,实行一下试试?

3.2 【问题】装饰者的弊端

想法虽好,可到了编码实行的时候,才发现装饰者的一个最大的弊端:**每个业务层接口都要写一个装饰者!**现在只有积分变动逻辑需要添加日志记录,回头用户修改密码也要加日志记录,那 UserService 也要写装饰者;部门发生变动也要加日志记录,那 DepartmentService 也就需要写装饰者,这编码量未免也太大了吧!

不行,装饰者模式解决这个问题不是上乘之选,那还有没有更简单的办法了呢?

3.3 【再次尝试】引入模板方法模式?

行为型模式中还有一个可以抽取逻辑的模式,那就是模板方法模式。在之前 IOC 部分的学习中,我们也看到了,SpringFramework 中大量运用了模板方法模式来控制底层的逻辑结构。那如果用模板方法模式来改造代码的话,我们倒是可以加一个 AbstractDemoService 类来抽取出日志的打印逻辑:

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);
    
    // 省略其余方法......
}

这样的抽取倒是也可以解决问题啦,DemoServiceImpl 就不再需要实现 DemoService 接口的 add 方法,而是只需要实现 doAdd 方法即可:

public class DemoServiceImpl extends AbstractDemoService implements DemoService {
    
    DemoDao demoDao = (DemoDao) BeanFactory.getBean("demoDao");
    
    @Override
    public List<String> findAll() {
        return demoDao.findAll();
    }
    
    // 只需要实现父类留下的doXXX方法即可
    @Override
    protected int doAdd(String userId, int points) {
        return points;
    }
    
    // 省略其余方法......
}

3.4 【问题】模板方法模式的弊端

但是。。。写到这里小伙伴们是不是感觉怪怪的。。。怎么感觉这样写下来,代码量比装饰者模式更多了???而且,还有一个很棘手的点:由于模板方法模式使用了继承,一个 DemoService 只能扩展一个功能了!这灵活性还不如装饰者模式呢!

不行,模板方法模式也解决不了这个问题,还有别的办法吗?

3.5 【再次尝试】引入责任链模式?

行为型模式中还有一种可以抽取逻辑的模式,那就是责任链模式。责任链最大的特点是将一个动作的请求放在一条对象的链子上传播,直到职责链上的某个对象能处理该请求时终止。用这种设计模式的话,针对多种不同功能的扩展,似乎这不像是责任链,更像是装饰者。。。所以我们可以这样理解,对于功能的扩展,装饰者跟责任链能实现的最终效果是几乎一致的。

但是。。。(又但是了),前面我们已经看过了,装饰者模式要编写的代码已经好多了,能不能省省了。。。少写点,真的好累哦!

3.6 【方案】代理模式

好了,前面那么多铺垫,就是为了引出代理模式。从代理的生成方式来看,代理分为静态代理和动态代理:静态代理需要自行编写代理类,组合原有的目标对象,并实现原有目标对象实现的接口,以此来做到对原有对象的方法功能的增强;动态代理只需要编写增强逻辑类,在运行时动态将增强逻辑类组合进原有的目标对象,即可生成代理对象,完成对目标对象的方法功能增强。

有关静态代理的内容,小册在此处不作讲解,小伙伴们可以自己动手写一写。

在实际编写中,可能会有小伙伴产生一种错觉:代理和装饰者的编写几乎一样啊,它俩有什么不同呢?是这样,代理模式侧重的是对原有目标对象的访问权限控制,而装饰者是在原有对象之上增强功能。

这样看起来似乎装饰者更适合完成这个功能,但这里面存在一个小问题:没有动态装饰者。。。(笑哭)所以我们就使用动态代理来尝试解决该问题了。

3.7 【思想】OOP的不足&横切的思想

从上面的几个设计模式的尝试和分析中,我们发现了一个 OOP 的很大的不足之处:诸如上面的这种相同、重复的逻辑,OOP 没有办法将这些逻辑分离出去,OOP 只能尽可能的减少这些重复的代码量,却无法避免重复代码的出现

再观察一下上面的代码和示意图,我们很明显可以发现,四个方法中的起始动作都是日志打印的方法,它们可以用一个横截的框 框起来:

Spring AOP入门-AOP是怎么来的

这种框可以是一个类的几个方法,可以是多个类的不同方法。只要这些方法的开始 / 结束都有相同的逻辑,那我们就可以把这些逻辑都拿出来视为一体,这个思想就叫横切,提取出来的逻辑组成的虚拟的结构,我们可以称之为横切面(上图的红框就可以理解为一个横切面)。

4. 最终方案-动态代理

好了终于可以引出动态代理了,Java 早在 jdk 1.3 中就引入动态代理了,具体的用法,以及 Cglib 的动态代理,咱下一章会作一个复习和回顾,这里咱先写一下看看效果。

既然是 Servlet 要依赖 DemoService ,那我们可以先这样,让 Servlet 在初始化的时候,从 BeanFactory 中获取 DemoService ,然后借助 jdk 动态代理生成 DemoService 的代理对象,并给其中的方法增强:

@WebServlet(urlPatterns = "/demo10")
public class DemoServlet10 extends HttpServlet {
    
    DemoService demoService;
    
    @Override
    public void init() throws ServletException {
        DemoService demoService = (DemoService) BeanFactory.getBean("demoService");
        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);
                });
    }
    // ......
}

然后,按照之前在 IOC 入门中的方法,部署好应用。

4.1 【问题】方法全部被增强

Tomcat 启动完成后,咱直接访问 /demo10 ,观察控制台的打印:

DemoService findAll ...
参数列表: null
DemoService add ...
参数列表: [bearbear, 666]
DemoService subtract ...
参数列表: [bearbear, 666]

add 方法和 subtract 方法有打印日志,这个一点毛病也没有,但是 findAll 怎么也打印了呢?很简单,jdk 的动态代理,本来就是给原有对象的所有方法都进行增强

但我们一开始的需求,是只给 add 、subtract 等积分变动的方法增强,这个要怎么办呢?

4.2 【方案】过滤方法

解决方案倒是很简单,在 DemoService 的代理对象创建逻辑中,添加方法名称的判断就 OK 了:

@Override
public void init() throws ServletException {
    DemoService demoService = (DemoService) BeanFactory.getBean("demoService");
    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);
            });
}

等一下,先不要着急重启,看看这 “糟糕” 的代码,想想有什么问题?

又是硬编码了啊!这些东西很明显可以分离到外部化配置中!所以我们可以在 resources 目录下再创建一个 proxy.properties ,在这个文件中定义日志打印的增强代理方法:

log.methods=add,subtract,multiply,divide

然后,由 Servlet 负责加载该配置文件,并创建 DemoService 的代理对象:

@Override
public void init() throws ServletException {
    // 读取proxy.properties
    Properties proxyProp = new Properties();
    try {
        proxyProp.load(this.getClass().getClassLoader().getResourceAsStream("proxy.properties"));
    } catch (IOException e) {
        throw new ExceptionInInitializerError("DemoServlet11 initialize error, cause: " + e.getMessage());
    }

    DemoService demoService = (DemoService) BeanFactory.getBean("demoService");
    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);
            });
}

重新启动 Tomcat ,刷新,可以发现这次只有 add 和 subtract 方法的执行中打印了日志:

DemoService add ...
参数列表: [bearbear, 666]
DemoService subtract ...
参数列表: [bearbear, 666]

到这里,基本上 AOP 的概念就快出来了,不过小伙伴先不要着急,咱继续往下推演。

5. 【问题】代理对象的创建者?

上面的代码编写中,小伙伴是否有一种感觉:代理对象应该由 Servlet 创建吗?那回头如果有好多个 Servlet 都依赖了这个 Service ,那岂不是要重复创建好多次?

很明显,Servlet 要拿到的 Service 应该是被代理过的吧!那这个问题应该如何解决呢?

当然是由 BeanFactory 帮忙创建啦!按道理讲,如果引入了代理的机制,那么 BeanFactory 创建的对象就应该是被增强过的代理对象!好,明确了需求之后,下面我们来试着改造一下原有的 BeanFactory 。

5.1 重新设计properties

之前的 factory.properties 中,我们只定义过 bean 的名称对应的类的全限定名:

demoService=com.linkedbear.architecture.l_proxyfactory.service.impl.DemoServiceImpl
demoDao=com.linkedbear.architecture.l_proxyfactory.dao.impl.DemoDaoImpl

这次加入代理的增强后,这些很明显就不够了,我们可以试着这样写一下代理的声明:

demoService=com.linkedbear.architecture.l_proxyfactory.service.impl.DemoServiceImpl
demoService.proxy.class=com.linkedbear.architecture.l_proxyfactory.proxy.LogAdvisor
demoService.proxy.methods=add,subtract,multiply,divide

demoDao=com.linkedbear.architecture.l_proxyfactory.dao.impl.DemoDaoImpl

看,我在 properties 中额外定义了 demoService 的增强类的全限定名,也声明了这个增强类要增强的方法列表,这样 BeanFactory 加载到 properties 中就能拿到这两个信息。

5.2 编写LogAdvisor

既然这次要把增强的 InvocationHandler 拿到外面单独实现,那就必须要编写新的类了,咱新建一个 proxy 包,把这个 LogAdvisor 放在这里。(这个类的命名也是一个伏笔啊)

public class LogAdvisor implements InvocationHandler {
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(???, args);
    }
}

等一下,这个 method 的 invoke 方法中,不是要拿被代理的对象吗?可是这里还没有呢,那我们用成员属性 + 构造器的方式,把原来的被代理对象传进来:

public class LogAdvisor implements InvocationHandler {
    
    private Object target;
    
    public LogAdvisor(Object target) {
        this.target = target;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(target, args);
    }
}

这样就做到全方法的增强了,不过咱前面还封装了 methods 呢(不要忘记需求哦),所以相应的,咱把 methods 也引入进来:

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);
    }
}

这样 LogAdvisor 的逻辑就写完了,相对还是比较简单的哈。

为了保持风格统一,在本章中如果编写其他的 Advisor ,也必须是像上面一样,声明一个两参数的构造方法。

5.3 修改BeanFactory

最后咱把 BeanFactory 修改一下,前面的需求中咱说了,要在 getBean 的时候把代理对象创建出来,所以此处要做一些逻辑的扩展了。

    Class<?> beanClazz = Class.forName(properties.getProperty(beanName));
    Object bean = beanClazz.newInstance();
    beanMap.put(beanName, bean);

这是原来的 getBean 方法中,通过了双检锁后真正创建 bean 对象的逻辑,这里咱要扩展了。

要使用动态代理生成代理对象,那么在创建完 bean 后,先不要着急放入 beanMap ,去看一看 properties 中是否有定义 proxy 相关的属性:

    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) {

    }
    
    beanMap.put(beanName, bean);

可见,如果获取到的 proxyAdvisorClassName 不为空,则代表这个 bean 有定义代理增强,需要反射创建 InvocationHandler 的实现类。

接下来,反射到 InvocationHandler 的实现类后,还要从 properties 中获取这个 bean 要增强(被代理)的方法列表:

    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);

这样写完之后,就可以创建对象了吧!由于前面我们在 LogAdvisor 中有定义过编码风格,所以这里一定可以获取到一个构造方法,而且是两个参数的:

    public LogAdvisor(Object target, String[] methods) {
        this.target = target;
        this.methods = Arrays.asList(methods);
    }

所以我们在 BeanFactory 中就可以这样反射创建 LogAdvisor 的对象:

    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);

经过这样的创建之后,原始对象、InvocationHandler 都有了,接下来就可以使用动态代理,创建代理对象了:

    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) {
        // 有定义代理增强,需要反射创建InvocationHandler的实现类
        Class<?> proxyAdvisorClass = Class.forName(proxyAdvisorClassName);
        
        // 从properties中找出当前bean需要增强的方法列表
        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);

5.4 还原Servlet

最后,把 Servlet 的逻辑改回之前的样子:

@WebServlet(urlPatterns = "/demo12")
public class DemoServlet12 extends HttpServlet {
    
    DemoService demoService = (DemoService) BeanFactory.getBean("demoService");
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().println(demoService.findAll().toString());
        demoService.add("bearbear", 666);
        demoService.subtract("bearbear", 666);
    }
}

5.5 测试运行

重启 Tomcat ,重新访问 Servlet 的路径,发现控制台依旧能正常打印日志:

com.linkedbear.architecture.l_proxyfactory.service.impl.DemoServiceImpl add ...
参数列表: [bearbear, 666]
com.linkedbear.architecture.l_proxyfactory.service.impl.DemoServiceImpl subtract ...
参数列表: [bearbear, 666]

证明 Servlet 拿到的 DemoService 已经是代理对象了。


到这里,整个场景也都演绎完毕了,跟 IOC 的引入一样,咱先总结一下整个过程中出现的几个关键点:

  • 传统的 GoF23 设计模式中可以一定程度上解决重复代码,但整体上编码量会激增
  • 使用动态代理可以在不改变原有逻辑的前提下,对已有方法增强
  • 创建代理对象应由 IOC 容器负责,而不是使用者本身

6. AOP的思想引入【重点】

最后,引出本章的主题。

Spring AOP入门-AOP是怎么来的

这个图中的红框我们说它称为横切面,英文表示为 Aspect ,它表示的是分布在一个 / 多个类的多个方法中的相同逻辑。利用动态代理,将这部分相同的逻辑抽取为一个独立的 Advisor 增强器,并在原始对象的初始化过程中,动态组合原始对象并产生代理对象,同样能完成一样的功能增强。在此基础上,通过指定增强的类名、方法名(甚至方法参数列表类型等),可以更细粒度的对方法增强。使用这种方式,可以在不修改原始代码的前提下,对已有任意代码的功能增强。而这种针对相同逻辑的扩展和抽取,就是所谓的面向切面编程(Aspect Oriented Programming,AOP)

从这一章开始,咱就算开始学习 AOP 了哈。跟 IOC 部分一样,我们还是先来了解一下 AOP 的演变。

上次在 IOC 部分我们用过的 Servlet 三层架构的工程还都有吧,我们在这上面继续做演变。

1. 【需求】日志记录

继 BeanFactory 抽取完成之后,你负责的项目最终是稳定的在客户的服务器上运行,客户给予了你高度的赞赏,一期的项目也就告一段落了。项目运行了大半年之后,有一天客户突然给你打来了电话:

靓仔啦,我们这边不知道咋回事,系统里出现了好多异常数据的啦,你有时间来帮我们看一下的啦 ~

本来这会你也不忙,就开了远程软件连上服务器看了一下,果不其然,在这个系统的积分模块里,有几个用户的积分异于常人的多。但是吧,系统里一开始就没把这个积分当做很重要的模块来设计,所以也没有什么积分的流水记录,也就没办法查这些积分的来源了。

你跟客户说明这个事情之后,客户显得比较着急:这不大行啊,积分也是很重要滴,能不能帮我们把积分的变动都记录下来啊,这样我们回头查起来也方便一些。起初你也没觉得这事多麻烦,就一口答应了。

代码预编写

为迎合接下来的剧情变动,我们需要做一些准备工作。

首先,复制 GitHub 中 spring-00-introduction 工程的 e_cachedfactory 目录,完整的拷贝一份出来到 f_pointslog :( Servlet 记得改名)

Spring AOP入门-AOP是怎么来的

接下来,resources 目录下的 factory_e.properties 也拷贝一份,命名为 factory_f.properties ,记得把里面的类名同步的改成 f 包的:

demoService=com.linkedbear.architecture.f_pointslog.service.impl.DemoServiceImpl
demoDao=com.linkedbear.architecture.f_pointslog.dao.impl.DemoDaoImpl

还有 BeanFactory 中的 properties 加载路径改掉:

static {
    properties = new Properties();
    try {
        // 记得改这里
        properties.load(BeanFactory.class.getClassLoader().getResourceAsStream("factory_f.properties"));
    } catch (IOException e) {
        throw new ExceptionInInitializerError("BeanFactory initialize error, cause: " + e.getMessage());
    }
}

然后,给 DemoService 添加几个方法,代表积分变动的逻辑:

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);
}

相应的,给 DemoServiceImpl 中添加对应的实现:(这里我们就不再拿着 DemoDao 折腾了,怪费劲的 … )

@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;
}

再复制一个相同的 DemoServiceImpl ,命名为 DemoServiceImpl2 ,代码不需要变。

最后,在 DemoServlet6 中,添加对 add 和 subtract 方法的调用:

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    resp.getWriter().println(demoService.findAll().toString());
    demoService.add("bearbear", 666);
    demoService.subtract("bearbear", 666);
}

到这里,代码的前期准备就搞定了,下面咱继续来讲故事。

2. 【初修改】积分变动逻辑添加

答应完事之后,你缓缓的打开了原来的工程,仔细搜查了一遍之后,发现事情不简单了:涉及到积分变动的逻辑好多啊!虽然改起来难度不大,但这么多改起来也挺费劲啊!

2.1 修改DemoServiceImpl

既然是加日志记录,那就在每个方法执行的一开始,先把这个类的类名 + 执行的方法名,以及参数都打印出来吧:

@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 subtract ...");
    System.out.println("user: " + userId + ", points: " + points);
    return points;
}

// 省略multiply与divide......

这代码写的,你是一边写一边骂啊,得亏这业务方法不多,要是来上个百儿八十个,那我不又得跟之前那帮孙子改数据库似的?

可是。。。聪明的你在写的时候,越来越感觉这事不对劲:哎这不对劲啊,这些日志的打印,它们的逻辑几乎都是一样的诶!我干嘛非得一个一个的这样写呢?最起码的,我封装个工具类也好啊!

2.2 封装LogUtils

于是,你的脑海中出现了这样的一个工具类:

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));
    }
}

这样改完之后,DemoServiceImpl 中就变成了这样子:

@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", "subtract", userId, points);
    return points;
}

// 省略multiply与divide......

好歹的少了一行代码吧,最起码复制粘贴起来能轻快点。。。

可是。。。也仅仅是少了一点吧,每个方法还是得写 LogUtils.printLog 方法,相当于没改!

Spring AOP入门-AOP是怎么来的

所以,有没有什么方法,能让这个日志打印的代码从核心 Service 中移除掉,但同样实现这个效果呢?(毕竟,当 Service 一旦大量增加之后,核心 Service 的逻辑会跟这些附加的动作混为一起,实在太过臃肿,代码会变得很难维护)

3. 引入设计模式

在 GoF23 设计模式中,其实有一些设计模式,是能解决一部分当下的这个问题的。(我知道很多小伙伴们都在想着代理模式了,不过小册想在此之前先讲点别的,顺便让小伙伴们加深一下以下引入的几个设计模式的对比)

3.1 【尝试】引入装饰者模式?

在行为型模式中,装饰者模式就可以给原有的逻辑上扩展额外的动作,看样子装饰者是可以解决这个问题的!所以我们可以尝试着这样改造一下原有的代码:

// 装饰者实现被装饰者同样的接口
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);
    }
    
    // 省略multiply与divide......
}

经过这样一包装后,在 DemoServlet 再获取 DemoService 的时候,就可以用 DemoServiceDecorator 包装一下了:

@WebServlet(urlPatterns = "/demo8")
public class DemoServlet8 extends HttpServlet {
    
    DemoService demoService = new DemoServiceDecorator((DemoService) BeanFactory.getBean("demoService"));

而且利用装饰者模式的特性,可以对原对象反复装饰!换言之, DemoService 可以增强的逻辑不仅仅是日志记录了,如果有事务也可以加入事务控制,如果有其它校验等逻辑也可以编写装饰者来包装它!

这个主意不错,实行一下试试?

3.2 【问题】装饰者的弊端

想法虽好,可到了编码实行的时候,才发现装饰者的一个最大的弊端:**每个业务层接口都要写一个装饰者!**现在只有积分变动逻辑需要添加日志记录,回头用户修改密码也要加日志记录,那 UserService 也要写装饰者;部门发生变动也要加日志记录,那 DepartmentService 也就需要写装饰者,这编码量未免也太大了吧!

不行,装饰者模式解决这个问题不是上乘之选,那还有没有更简单的办法了呢?

3.3 【再次尝试】引入模板方法模式?

行为型模式中还有一个可以抽取逻辑的模式,那就是模板方法模式。在之前 IOC 部分的学习中,我们也看到了,SpringFramework 中大量运用了模板方法模式来控制底层的逻辑结构。那如果用模板方法模式来改造代码的话,我们倒是可以加一个 AbstractDemoService 类来抽取出日志的打印逻辑:

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);
    
    // 省略其余方法......
}

这样的抽取倒是也可以解决问题啦,DemoServiceImpl 就不再需要实现 DemoService 接口的 add 方法,而是只需要实现 doAdd 方法即可:

public class DemoServiceImpl extends AbstractDemoService implements DemoService {
    
    DemoDao demoDao = (DemoDao) BeanFactory.getBean("demoDao");
    
    @Override
    public List<String> findAll() {
        return demoDao.findAll();
    }
    
    // 只需要实现父类留下的doXXX方法即可
    @Override
    protected int doAdd(String userId, int points) {
        return points;
    }
    
    // 省略其余方法......
}

3.4 【问题】模板方法模式的弊端

但是。。。写到这里小伙伴们是不是感觉怪怪的。。。怎么感觉这样写下来,代码量比装饰者模式更多了???而且,还有一个很棘手的点:由于模板方法模式使用了继承,一个 DemoService 只能扩展一个功能了!这灵活性还不如装饰者模式呢!

不行,模板方法模式也解决不了这个问题,还有别的办法吗?

3.5 【再次尝试】引入责任链模式?

行为型模式中还有一种可以抽取逻辑的模式,那就是责任链模式。责任链最大的特点是将一个动作的请求放在一条对象的链子上传播,直到职责链上的某个对象能处理该请求时终止。用这种设计模式的话,针对多种不同功能的扩展,似乎这不像是责任链,更像是装饰者。。。所以我们可以这样理解,对于功能的扩展,装饰者跟责任链能实现的最终效果是几乎一致的。

但是。。。(又但是了),前面我们已经看过了,装饰者模式要编写的代码已经好多了,能不能省省了。。。少写点,真的好累哦!

3.6 【方案】代理模式

好了,前面那么多铺垫,就是为了引出代理模式。从代理的生成方式来看,代理分为静态代理和动态代理:静态代理需要自行编写代理类,组合原有的目标对象,并实现原有目标对象实现的接口,以此来做到对原有对象的方法功能的增强;动态代理只需要编写增强逻辑类,在运行时动态将增强逻辑类组合进原有的目标对象,即可生成代理对象,完成对目标对象的方法功能增强。

有关静态代理的内容,小册在此处不作讲解,小伙伴们可以自己动手写一写。

在实际编写中,可能会有小伙伴产生一种错觉:代理和装饰者的编写几乎一样啊,它俩有什么不同呢?是这样,代理模式侧重的是对原有目标对象的访问权限控制,而装饰者是在原有对象之上增强功能。

这样看起来似乎装饰者更适合完成这个功能,但这里面存在一个小问题:没有动态装饰者。。。(笑哭)所以我们就使用动态代理来尝试解决该问题了。

3.7 【思想】OOP的不足&横切的思想

从上面的几个设计模式的尝试和分析中,我们发现了一个 OOP 的很大的不足之处:诸如上面的这种相同、重复的逻辑,OOP 没有办法将这些逻辑分离出去,OOP 只能尽可能的减少这些重复的代码量,却无法避免重复代码的出现

再观察一下上面的代码和示意图,我们很明显可以发现,四个方法中的起始动作都是日志打印的方法,它们可以用一个横截的框 框起来:

Spring AOP入门-AOP是怎么来的

这种框可以是一个类的几个方法,可以是多个类的不同方法。只要这些方法的开始 / 结束都有相同的逻辑,那我们就可以把这些逻辑都拿出来视为一体,这个思想就叫横切,提取出来的逻辑组成的虚拟的结构,我们可以称之为横切面(上图的红框就可以理解为一个横切面)。

4. 最终方案-动态代理

好了终于可以引出动态代理了,Java 早在 jdk 1.3 中就引入动态代理了,具体的用法,以及 Cglib 的动态代理,咱下一章会作一个复习和回顾,这里咱先写一下看看效果。

既然是 Servlet 要依赖 DemoService ,那我们可以先这样,让 Servlet 在初始化的时候,从 BeanFactory 中获取 DemoService ,然后借助 jdk 动态代理生成 DemoService 的代理对象,并给其中的方法增强:

@WebServlet(urlPatterns = "/demo10")
public class DemoServlet10 extends HttpServlet {
    
    DemoService demoService;
    
    @Override
    public void init() throws ServletException {
        DemoService demoService = (DemoService) BeanFactory.getBean("demoService");
        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);
                });
    }
    // ......
}

然后,按照之前在 IOC 入门中的方法,部署好应用。

4.1 【问题】方法全部被增强

Tomcat 启动完成后,咱直接访问 /demo10 ,观察控制台的打印:

DemoService findAll ...
参数列表: null
DemoService add ...
参数列表: [bearbear, 666]
DemoService subtract ...
参数列表: [bearbear, 666]

add 方法和 subtract 方法有打印日志,这个一点毛病也没有,但是 findAll 怎么也打印了呢?很简单,jdk 的动态代理,本来就是给原有对象的所有方法都进行增强

但我们一开始的需求,是只给 add 、subtract 等积分变动的方法增强,这个要怎么办呢?

4.2 【方案】过滤方法

解决方案倒是很简单,在 DemoService 的代理对象创建逻辑中,添加方法名称的判断就 OK 了:

@Override
public void init() throws ServletException {
    DemoService demoService = (DemoService) BeanFactory.getBean("demoService");
    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);
            });
}

等一下,先不要着急重启,看看这 “糟糕” 的代码,想想有什么问题?

又是硬编码了啊!这些东西很明显可以分离到外部化配置中!所以我们可以在 resources 目录下再创建一个 proxy.properties ,在这个文件中定义日志打印的增强代理方法:

log.methods=add,subtract,multiply,divide

然后,由 Servlet 负责加载该配置文件,并创建 DemoService 的代理对象:

@Override
public void init() throws ServletException {
    // 读取proxy.properties
    Properties proxyProp = new Properties();
    try {
        proxyProp.load(this.getClass().getClassLoader().getResourceAsStream("proxy.properties"));
    } catch (IOException e) {
        throw new ExceptionInInitializerError("DemoServlet11 initialize error, cause: " + e.getMessage());
    }

    DemoService demoService = (DemoService) BeanFactory.getBean("demoService");
    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);
            });
}

重新启动 Tomcat ,刷新,可以发现这次只有 add 和 subtract 方法的执行中打印了日志:

DemoService add ...
参数列表: [bearbear, 666]
DemoService subtract ...
参数列表: [bearbear, 666]

到这里,基本上 AOP 的概念就快出来了,不过小伙伴先不要着急,咱继续往下推演。

5. 【问题】代理对象的创建者?

上面的代码编写中,小伙伴是否有一种感觉:代理对象应该由 Servlet 创建吗?那回头如果有好多个 Servlet 都依赖了这个 Service ,那岂不是要重复创建好多次?

很明显,Servlet 要拿到的 Service 应该是被代理过的吧!那这个问题应该如何解决呢?

当然是由 BeanFactory 帮忙创建啦!按道理讲,如果引入了代理的机制,那么 BeanFactory 创建的对象就应该是被增强过的代理对象!好,明确了需求之后,下面我们来试着改造一下原有的 BeanFactory 。

5.1 重新设计properties

之前的 factory.properties 中,我们只定义过 bean 的名称对应的类的全限定名:

demoService=com.linkedbear.architecture.l_proxyfactory.service.impl.DemoServiceImpl
demoDao=com.linkedbear.architecture.l_proxyfactory.dao.impl.DemoDaoImpl

这次加入代理的增强后,这些很明显就不够了,我们可以试着这样写一下代理的声明:

demoService=com.linkedbear.architecture.l_proxyfactory.service.impl.DemoServiceImpl
demoService.proxy.class=com.linkedbear.architecture.l_proxyfactory.proxy.LogAdvisor
demoService.proxy.methods=add,subtract,multiply,divide

demoDao=com.linkedbear.architecture.l_proxyfactory.dao.impl.DemoDaoImpl

看,我在 properties 中额外定义了 demoService 的增强类的全限定名,也声明了这个增强类要增强的方法列表,这样 BeanFactory 加载到 properties 中就能拿到这两个信息。

5.2 编写LogAdvisor

既然这次要把增强的 InvocationHandler 拿到外面单独实现,那就必须要编写新的类了,咱新建一个 proxy 包,把这个 LogAdvisor 放在这里。(这个类的命名也是一个伏笔啊)

public class LogAdvisor implements InvocationHandler {
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(???, args);
    }
}

等一下,这个 method 的 invoke 方法中,不是要拿被代理的对象吗?可是这里还没有呢,那我们用成员属性 + 构造器的方式,把原来的被代理对象传进来:

public class LogAdvisor implements InvocationHandler {
    
    private Object target;
    
    public LogAdvisor(Object target) {
        this.target = target;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(target, args);
    }
}

这样就做到全方法的增强了,不过咱前面还封装了 methods 呢(不要忘记需求哦),所以相应的,咱把 methods 也引入进来:

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);
    }
}

这样 LogAdvisor 的逻辑就写完了,相对还是比较简单的哈。

为了保持风格统一,在本章中如果编写其他的 Advisor ,也必须是像上面一样,声明一个两参数的构造方法。

5.3 修改BeanFactory

最后咱把 BeanFactory 修改一下,前面的需求中咱说了,要在 getBean 的时候把代理对象创建出来,所以此处要做一些逻辑的扩展了。

    Class<?> beanClazz = Class.forName(properties.getProperty(beanName));
    Object bean = beanClazz.newInstance();
    beanMap.put(beanName, bean);

这是原来的 getBean 方法中,通过了双检锁后真正创建 bean 对象的逻辑,这里咱要扩展了。

要使用动态代理生成代理对象,那么在创建完 bean 后,先不要着急放入 beanMap ,去看一看 properties 中是否有定义 proxy 相关的属性:

    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) {

    }
    
    beanMap.put(beanName, bean);

可见,如果获取到的 proxyAdvisorClassName 不为空,则代表这个 bean 有定义代理增强,需要反射创建 InvocationHandler 的实现类。

接下来,反射到 InvocationHandler 的实现类后,还要从 properties 中获取这个 bean 要增强(被代理)的方法列表:

    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);

这样写完之后,就可以创建对象了吧!由于前面我们在 LogAdvisor 中有定义过编码风格,所以这里一定可以获取到一个构造方法,而且是两个参数的:

    public LogAdvisor(Object target, String[] methods) {
        this.target = target;
        this.methods = Arrays.asList(methods);
    }

所以我们在 BeanFactory 中就可以这样反射创建 LogAdvisor 的对象:

    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);

经过这样的创建之后,原始对象、InvocationHandler 都有了,接下来就可以使用动态代理,创建代理对象了:

    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) {
        // 有定义代理增强,需要反射创建InvocationHandler的实现类
        Class<?> proxyAdvisorClass = Class.forName(proxyAdvisorClassName);
        
        // 从properties中找出当前bean需要增强的方法列表
        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);

5.4 还原Servlet

最后,把 Servlet 的逻辑改回之前的样子:

@WebServlet(urlPatterns = "/demo12")
public class DemoServlet12 extends HttpServlet {
    
    DemoService demoService = (DemoService) BeanFactory.getBean("demoService");
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().println(demoService.findAll().toString());
        demoService.add("bearbear", 666);
        demoService.subtract("bearbear", 666);
    }
}

5.5 测试运行

重启 Tomcat ,重新访问 Servlet 的路径,发现控制台依旧能正常打印日志:

com.linkedbear.architecture.l_proxyfactory.service.impl.DemoServiceImpl add ...
参数列表: [bearbear, 666]
com.linkedbear.architecture.l_proxyfactory.service.impl.DemoServiceImpl subtract ...
参数列表: [bearbear, 666]

证明 Servlet 拿到的 DemoService 已经是代理对象了。


到这里,整个场景也都演绎完毕了,跟 IOC 的引入一样,咱先总结一下整个过程中出现的几个关键点:

  • 传统的 GoF23 设计模式中可以一定程度上解决重复代码,但整体上编码量会激增
  • 使用动态代理可以在不改变原有逻辑的前提下,对已有方法增强
  • 创建代理对象应由 IOC 容器负责,而不是使用者本身

6. AOP的思想引入【重点】

最后,引出本章的主题。

Spring AOP入门-AOP是怎么来的

这个图中的红框我们说它称为横切面,英文表示为 Aspect ,它表示的是分布在一个 / 多个类的多个方法中的相同逻辑。利用动态代理,将这部分相同的逻辑抽取为一个独立的 Advisor 增强器,并在原始对象的初始化过程中,动态组合原始对象并产生代理对象,同样能完成一样的功能增强。在此基础上,通过指定增强的类名、方法名(甚至方法参数列表类型等),可以更细粒度的对方法增强。使用这种方式,可以在不修改原始代码的前提下,对已有任意代码的功能增强。而这种针对相同逻辑的扩展和抽取,就是所谓的面向切面编程(Aspect Oriented Programming,AOP)

 

免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » Spring AOP入门-AOP是怎么来的