博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Spring Aop 应用实例与设计浅析
阅读量:5221 次
发布时间:2019-06-14

本文共 16880 字,大约阅读时间需要 56 分钟。

0.代码概述

代码说明:第一章中的代码为了突出模块化拆分的必要性,所以db采用了真实操作。下面代码中dao层使用了打印日志模拟插入db的方法,方便所有人运行demo。

1.项目代码地址:

2.结构化拆分,代码包结构:org.kingszelda.version1

3.Spring AOP,代码包结构:org.kingszelda.version2

4.AspectJ AOP,代码包结构:org.kingszelda.version3

1.为什么会出现AOP

  相信很多人和我一样,编程入门从c语言开始,之后接触的java等其他面向对象语言。刚接触编程语言时编写的代码以实现功能为首要目标,因此很少考虑模块化、封装等因素,以一个计算功能的web项目为例。该web项目有如下功能:

  •  通过http接口提供加法计算功能
  •  计算请求与结果需要留存
  •  需统计接口调用次数,打印请求响应日志,打印方法运行时间

  就算基础java语言而言,这个功能也比较简单。如果不考虑http协议的问题,单独编写demo功能只需要一部分。

import java.sql.Connection;import java.sql.DriverManager;import java.sql.PreparedStatement;import java.util.HashMap;import java.util.Map;import org.slf4j.Logger;import org.slf4j.LoggerFactory;/** * 计算器 */public class CalculateService {    private static final Logger logger = LoggerFactory.getLogger(CalculateService.class);    //1.构造接口调用次数计数Map    public Map
countMap = new HashMap
(); public int add(int first, int second) throws Exception { //2.获得计算开始时间 long start = System.currentTimeMillis(); //3.打印入口参数 logger.info("方法入参为:{}{}", first, second); //4.将该方法调用次数+1后放入map countMap.put("calcAdd", count); //5.计算加法 int result = first + second; //6.加载mysql驱动 Class.forName("com.mysql.jdbc.Driver"); //7.配置mysql连接属性,地址、用户名、密码 String url = "jdbc:mysql://localhost:3306/samp_db"; String userName = "root"; String passWord = "123456"; //8.获得mysql连接 Connection conn = DriverManager.getConnection(url, userName, passWord); //9.生成插入sql String sql = "INSERT INTO calcAdd (`first`,`second`,`result`) VALUES(?,?,?)"; //10.使用preparedStatement防止sql注入 PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, String.valueOf(first)); ps.setString(2, String.valueOf(second)); ps.setString(3, String.valueOf(result)); //11.执行sql ps.execute(); //12.释放sql与con连接 ps.close(); conn.close(); //13.打印返回参数 logger.info("方法结果为:{}", result); //14.打印方法总耗时 logger.info("运行时间:{}ms", System.currentTimeMillis() - start); //13.返回计算结果 return result; }}

 

1.1 模块化

  上述代码完全可以满足要求,只是看起来有点长,当新增减法计算器等其他计算功能的时候,新增的代码重复的很多,比如计算的“+”号换成“-”号,存入数据库表从加法表换到减法表,然后计算接口调用次数。从模块化的角度来看,上面的加法计算器代码就可以做如下拆分:

import java.sql.Connection;import java.sql.DriverManager;import java.sql.PreparedStatement;import java.sql.Statement;/** * 数据库工具类 */public class DbUtil {    public static Connection getConnection() throws Exception {        //1.加载mysql驱动        Class.forName("com.mysql.jdbc.Driver");        //2.配置mysql连接属性,地址、用户名、密码        String url = "jdbc:mysql://localhost:3306/samp_db";        String userName = "root";        String passWord = "123456";        //3.获得mysql连接        return DriverManager.getConnection(url, userName, passWord);    }    public static void closeCon(Connection connection, Statement statement) throws Exception {        //1.先关闭sql连接        statement.close();        //2.再关闭数据库连接        connection.close();    }    public static Statement getInsertAddStatement(int first, int second, int result, Connection connection) throws Exception {        //7.生成插入sql        String sql = "INSERT INTO calcAdd (`first`,`second`,`result`) VALUES(?,?,?)";        //8.使用preparedStatement防止sql注入        PreparedStatement ps = connection.prepareStatement(sql);        ps.setString(1, String.valueOf(first));        ps.setString(2, String.valueOf(second));        ps.setString(3, String.valueOf(result));        return ps;    }}
import java.util.HashMap;import java.util.Map;/** * 方法调用次数计数器 */public class CountUtil {    public static Map
countMap = new HashMap<>(); public static void countMethod(String methodName) { Integer count = countMap.get(methodName); count = (count != null) ? new Integer(count + 1) : new Integer(1); countMap.put(methodName, count); }}
import java.sql.Connection;import java.sql.DriverManager;import java.sql.PreparedStatement;import java.util.HashMap;import java.util.Map;import org.slf4j.Logger;import org.slf4j.LoggerFactory;/** * 计算器 */public class CalculateService {    private static final Logger logger = LoggerFactory.getLogger(CalculateService.class);    //1.构造接口调用次数计数Map    public Map
countMap = new HashMap
(); public int add(int first, int second) throws Exception { //2.获得计算开始时间 long start = System.currentTimeMillis(); //3.打印入口参数 logger.info("方法入参为:{}{}", first, second); //4.将该方法调用次数+1后放入map CountUtil.countMethod("calcAdd"); //5.计算加法 int result = first + second; //6.数据库操作 Connection conn = DbUtil.getConnection(); Statement ps = DbUtil.getInsertAddStatement(first, second, result, conn); DbUtil.closeCon(conn, ps); //7.打印返回参数 logger.info("方法结果为:{}", result); //8.打印方法总耗时 logger.info("运行时间:{}ms", System.currentTimeMillis() - start); //9.返回计算结果 return result; }}

 

经过上述拆分,功能由原来的一块代码变为2大块与3小块,大块分别是:

  1. 数据库相关处理
  2. 方法调用计数处理

小块则是:

  1. 打印请求
  2. 打印方法耗时
  3. 打印响应
  4. 统计方法调用次数

  经过拆分以后,代码的复用性增强了很多,模块之间的边界也变得很清晰。并且随着设计模式的发展,上面的2大块可以进行进一步抽象,可以抽象出一个统一的主逻辑,然后加法计算进一步抽象为运算模块,这样就可以通过派生支持减法乘法等其他方法运算。这是数据设计模式的内容,这里不做赘述。此时代码拆分逻辑显然是垂直拆分,如图:

  如图所示,模块化之前,代码是一整块,当功能越来越饿复杂之后,这块代码将无法区分边界,变得不好维护。模块化之后,代码分模块独立,边界清晰、好维护。由于代码总是一行一行的从上到下执行,所以很自然的拆分逻辑就是从上到下纵向拆分。

  但是当我们仔细分析不同模块之间的区别之后,现有的纵向拆分并非达到了最佳状态。因为大块之间虽然清晰了,但是小块之间还是散落在各处,并且都是简单的两行,无法进行进一步封装。并且代码模块的业务也不相同。比如上述的存入数据库与计算方法调用次数之间就有区别。数据库模块关心具体业务,比如是哪个数据库,那张表,插入语句的具体内容。但是计算方法调用次数模块是不关心业务的,只是对调用次数进行统计,这种模块的应用有很多,比如打印日志,计算qps,计算运行时间等,不论是什么业务,这种运算总是相同的。所以,当业务变复杂之后,这些代码可以进行横向拆分。

至此,我们可以先给出模块化拆分之后的代码:

此时的代码结构是:

此时的代码为:

package org.kingszelda.version1.service;import org.kingszelda.common.dao.AddDao;import org.kingszelda.common.dao.SubDao;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Service;import org.springframework.web.bind.annotation.RequestMapping;import javax.annotation.Resource;/** * Created by shining.cui on 2017/7/15. */@Service@RequestMapping("version1")public class CalculateService {    private static final Logger logger = LoggerFactory.getLogger(CalculateService.class);    @Resource    private AddDao addDao;    @Resource    private SubDao subDao;    @Resource    private MethodCounter methodCounter;    public int add(int first, int second) {        //1.获得计算开始时间        long start = System.currentTimeMillis();        //2.打印入口参数        logger.info("方法入参为:{}{}", first, second);        //3.计算调用次数        methodCounter.count("sub");        //4.计算加法        int result = first + second;        //5.插入数据库        addDao.insert(first, second, result);        //6.打印返回参数        logger.info("方法结果为:{}", result);        //7.打印方法总耗时        logger.info("运行时间:{}ms", System.currentTimeMillis() - start);        //8.返回结果        return result;    }    public int sub(int first, int second) {        //1.获得计算开始时间        long start = System.currentTimeMillis();        //2.打印入口参数        logger.info("方法入参为:{}{}", first, second);        //3.计算调用次数        methodCounter.count("sub");        //4.计算加法        int result = first - second;        //5.插入数据库        subDao.insert(first, second, result);        //6.打印返回参数        logger.info("sub 方法结果为:{}", result);        //7.打印方法总耗时        logger.info("运行时间:{}ms", System.currentTimeMillis() - start);        //8.返回结果        return result;    }}
package org.kingszelda.version1.service;import com.google.common.collect.Maps;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;import java.util.Map;/** * Created by shining.cui on 2017/7/15. */@Componentpublic class MethodCounter {    private static final Logger logger = LoggerFactory.getLogger(MethodCounter.class);    //防止并发    private static final Map
methodCountMap = Maps.newConcurrentMap(); /** * 根据方法名进行调用次数计数 */ public void count(String methodName) { Integer methodCount = methodCountMap.get(methodName); methodCount = (methodCount != null) ? new Integer(methodCount + 1) : new Integer(1); logger.info("对方法{}进行次数加1,当前次数为:{}", methodName, methodCount); methodCountMap.put(methodName, methodCount); } public Map
getMethodCountMap() { return methodCountMap; }}

1.2 切面织入

  正如上面的代码一样,我们的项目开始支持加法与减法两种计算,此时的纵向拆分的代码是这样的结构:

这样的结构也很清晰,没有任何问题,但是存在了一些小瑕疵,那就是违反了DRY法则(Don't Repeat Yourself):计算次数、打印求响应、打印方法耗时出现在不同的程序的相同位置。虽然模块化之后的这两个功能调用都只需要一行,但是依然是发生了重复,这时候就是Aop登场的最佳时刻。

  当时用了Aop横向拆分之后,业务模块就只关心业务(加法计算器只关心加法计算与存入加法表),不用再关心一些通用的处理功能——日志与qps。这时候的代码发生了本质上的改变,首先需要一个Aop模块功能,然后通过"配置"的方式横向“织入”想要的代码模块中。这时候就算新增了乘法计算器,也只需要编写业务功能——“乘法计算与入库”,然后配置之前的Aop模块即可生效。

  从图中我们可以看到,进行切面编程有三个步骤:

  1. 定义切面功能,Advice即通知,比如打印请求,计算调用次数的功能。
  2. 定义切点,Pointcut即切点,比如开始的时候打印请求,结束的时候打印响应。对应功能1的调用时间定义。
  3. 组织功能,即Advisor通知器,将切面功能织入切点上。

  是时候引出Aop的定义了,以下定义引自维基百科:

面向侧面的程序设计(aspect-oriented programming,AOP,又译作面向方面的程序设计观点导向编程剖面导向程序设计)是中的一个术语,指一种。该范型以一种称为侧面(aspect,又译作方面)的语言构造为基础,侧面是一种新的模块化机制,用来描述分散在、或中的横切关注点(crosscutting concern)。

2.AOP与Spring AOP

  AOP的出现使得代码的整体设计括了一个维度,竖向写业务逻辑,横向切面写公共逻辑。如同其他概念一样,这项技术有着各种各样的实现,比较著名的有AspectJ,Javassist等。为了制定统一规范,于是出现了AOP联盟来起引导与约束的作用。

  正如上图所示,切面拆分场景一般分为4种情况:

  1. 进入业务之前,比如统计qps,打印请求参数
  2. 完成业务之后,比如打印响应结果
  3. 环绕业务,比如计算方法耗时,需要在业务前计时,业务后取时间差
  4. 业务抛异常后,比如统一的异常处理。这一点上面的代码没有体现。

  上面4种情况的的间隔其实比较模糊,比如环绕业务其实可以包含前、后、异常这三种情况,因为本质上都是在业务运行前后加通用逻辑,其实Spring AOP的AfterReturningAdvice,MethodBeforeAdvice,ThrowsAdvice也是基于MethodInterceptor的环绕业务实现的。

  对于Spring来说,其核心模块是IoC与AOP,其AOP是与IoC结合使用的。Spring不仅支持本身的Aop实现,同时也支持AspectJ的AOP功能。因此我们说:

  1. AOP是一种技术规范,本身与Spring无关
  2. Spring 实现了自身的AOP,即Spring AOP
  3. Spring 同时支持AspectJ的AOP功能

3.使用Spring AOP架构的代码

使用Spring AOP调整后的业务代码得到了一定的精简,整体代码结构如图:

首先是定义三个切面业务Advice,进行打印日志、运行时间与统计qps功能。

package org.kingszelda.version2.aop;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.aop.AfterReturningAdvice;import org.springframework.stereotype.Component;import java.lang.reflect.Method;/** * 方法后切面,打印响应结果 * Created by shining.cui on 2017/7/15. */@Componentpublic class CalculateAfterAdvice implements AfterReturningAdvice {    private static final Logger logger = LoggerFactory.getLogger(CalculateAfterAdvice.class);    public void afterReturning(Object returnObject, Method method, Object[] args, Object target) throws Throwable {        String methodName = method.getDeclaringClass().getSimpleName() + "." + method.getName();        String returnValue = String.valueOf(returnObject);        logger.info("方法{}的响应结果为{}", methodName, returnValue);    }}
package org.kingszelda.version2.aop;import org.kingszelda.version2.service.MethodCounter;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.aop.MethodBeforeAdvice;import org.springframework.stereotype.Component;import java.lang.reflect.Method;import java.util.Arrays;/** * 方法前切面,打印请求参数,统计调用次数 * Created by shining.cui on 2017/7/15. */@Componentpublic class CalculateBeforeAdvice extends MethodCounter implements MethodBeforeAdvice {    private static final Logger logger = LoggerFactory.getLogger(CalculateBeforeAdvice.class);    public void before(Method method, Object[] args, Object target) throws Throwable {        String methodName = method.getDeclaringClass().getSimpleName() + "." + method.getName();        String argStr = Arrays.toString(args);        logger.info("方法{}的请求参数为{}", methodName, argStr);        count(methodName);    }}
package org.kingszelda.version2.aop;import org.aopalliance.intercept.MethodInvocation;import org.kingszelda.version2.service.CalculateService;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.aop.IntroductionInterceptor;import org.springframework.stereotype.Component;/** * 方法后切面,打印响应结果 * Created by shining.cui on 2017/7/15. */@Componentpublic class CalculateMethodInterceptor implements IntroductionInterceptor {    private static final Logger logger = LoggerFactory.getLogger(CalculateMethodInterceptor.class);    public Object invoke(MethodInvocation methodInvocation) throws Throwable {        //1.获得计算开始时间        long start = System.currentTimeMillis();        String methodName = methodInvocation.getMethod().getDeclaringClass().getSimpleName() + "." + methodInvocation.getMethod().getName();        //2.运行程序        Object proceed = methodInvocation.proceed();        //3.打印间隔时间        logger.info("方法{}运行时间:{}ms", methodName, System.currentTimeMillis() - start);        return proceed;    }    public boolean implementsInterface(Class
aClass) { //满足CalculateService接口的方法都进行拦截 return aClass.isAssignableFrom(CalculateService.class); }}

此时的业务代码精简为如下:

package org.kingszelda.version2.service.impl;import org.kingszelda.common.dao.AddDao;import org.kingszelda.common.dao.SubDao;import org.kingszelda.version2.service.CalculateService;import javax.annotation.Resource;/** * Created by shining.cui on 2017/7/15. */public class CalculateServiceImpl implements CalculateService {    @Resource    private AddDao addDao;    @Resource    private SubDao subDao;    @Override    public int add(int first, int second) {        //1.计算加法        int result = first + second;        //2.插入数据库        addDao.insert(first, second, result);        //3.返回结果        return result;    }    @Override    public int sub(int first, int second) {        //1.计算加法        int result = first - second;        //2.插入数据库        subDao.insert(first, second, result);        //3.返回结果        return result;    }}

  到目前为止,切面逻辑已经与业务逻辑分离,接下来需要做的就是定义切点PointCut与通知器Advisor,即将业务与切面在合适的时候组合起来。这也是最容易出错的地方。

calculateMethodInterceptor
calculateBeforeAdvice
calculateAfterAdvice

  可以看到,Spring AOPde 配置方式可以分为两种:一种是手动配置被代理对象,只对配置对象织入。另外一种是只声明Advisor通知器,由Spring 根据定义的切点PointCut进行织入。两种方法都可以,第一种更贴近于源码的实现。

  至此,基于Spring AOP的整合已经结束,运行代码后发现虽然业务没有关系日志等操作,但切面逻辑已经完全织入业务中,业务代码整洁了好多。

  这里需要注意一点,我们已经不能像之前使用calculateService那样直接注入使用了,因为我们需要使用的是calculateService的代理类,这个代理类在调用真正的业务实现之前根据配置依次调用切面逻辑。上面的配置很明朗,我们使用的calculateServiceV2是由ProxyFactoryBean生成的对于接口proxyInterfaces的Proxy实现,真正被代理的对象对象是target,在调用target之前会依次执行interceptorNames配置的拦截器责任链。与此相关的代码解释的文章很多,这里就扒源码了。

4.Spring集成AspectJ的AOP功能

  在我看来Spring的AOP功能做的不够有决心,要么就定义好各种Spring AOP且切面场景,要么则都使用Intercepter由用户统一管理。两者都兼顾的Spring AOP在让用户使用的时候会产生一定的困惑,究竟使用那种方式更加好一些。并且Spring的老毛病也凸现出来,那就是xml如何妥善配置。

  而Spring对AspectJ的集成则方便了很多。项目结构如图:

相对于Spring AOP时的代码,变动只有一个切面类与xml配置

package org.kingszelda.version3.aop;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;import java.util.Arrays;/** * 基于AspectJ的切面功能 * * @author shining.cui * @since 2017-07-16 */@Aspect@Componentpublic class ControllerAop extends MethodCounter {    private static final Logger logger = LoggerFactory.getLogger(ControllerAop.class);    // 拦截version3包中的所有service    @Pointcut("(execution(* org.kingszelda.version3.service.*.*(..)))")    public void pointcut() {    }    @Around("pointcut()")    public Object controllerHandler(ProceedingJoinPoint joinPoint) {        String signature = joinPoint.getSignature().toShortString();        //1.统计调用次数        count(signature);        String argStr = Arrays.toString(joinPoint.getArgs());        //2.打印请求        logger.info("方法{}的请求参数为{}", signature, argStr);        Object result = null;        //3.获得计算开始时间        long start = System.currentTimeMillis();        try {            //4.运行业务            result = joinPoint.proceed();        } catch (Throwable e) {            logger.error("web 应用发生异常", e);        }        //5.打印运行时间        logger.info("方法{}运行时间:{}ms", signature, System.currentTimeMillis() - start);        String returnValue = String.valueOf(result);        //6.打印响应        logger.info("方法{}的响应结果为{}", signature, returnValue);        return result;    }}

  配置也简单了很多:

  程序运行结果与上一章相同。

5.总结

  Spring AOP是AOP联盟规范中Spring的一种实现形式,同时Spring也支持了AspectJ的AOP实现。相对于应用来说,AspectJ的AOP结构更加清晰,配置也更加简单。Spring中的AOP实现都是通过代理完成的,在默认的情况下,如果代理类是接口,则使用jdk动态代理,如果不是则使用CGLIB进行代理。

  通过AOP功能可以很好的将通用逻辑与业务逻辑分离,使得结构化更明朗。对于打印日志,统计qps,统计运行时间,发送消息,写入请求记录表等操作均可以很好的支持。Spring框架本身的一些功能也是基于AOP实现的,比如Spring的事务管理。因此研究Spring AOP总是会给我们的学习与工作带来好处。

  最后,以上代码均可运行,地址参见第0章。

转载于:https://www.cnblogs.com/kingszelda/p/7138142.html

你可能感兴趣的文章
Python命名规范
查看>>
滚动条
查看>>
程序员的自我修养九Windows下的动态链接
查看>>
Codeforces Round #361 (Div. 2)
查看>>
细说WebSocket - Node篇
查看>>
Extjs控件之 grid打印功能
查看>>
枚举类型(不常用)递归
查看>>
minggw 安装
查看>>
Jquery操作cookie,实现简单的记住用户名的操作
查看>>
[BZOJ1196][HNOI2006]公路修建问题 二分答案+最小生成树
查看>>
【原创】大数据基础之Zookeeper(4)应用场景
查看>>
静态变量数组实现LRU算法
查看>>
中文系统 上传file的input显示英文
查看>>
比callback更简洁的链式执行promise
查看>>
android permission
查看>>
【译】在Asp.Net中操作PDF - iTextSharp - 使用字体
查看>>
事务备份还原分离附加
查看>>
.net 文本框只允许输入XX,(正则表达式)
查看>>
[BSGS][哈希]luogu P3846 可爱的质数
查看>>
Python 第四十五章 MySQL 内容回顾
查看>>