一、概述

  AOP(Aspect Oriented Programming)称为面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等等,Struts2的拦截器设计就是基于AOP的思想,是个比较经典的例子。在不改变原有的逻辑的基础上,增加一些额外的功能。代理也是这个功能,读写分离也能用 AOP 来做。

  AOP可以说是 OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

  AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

  使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

二、问题解决方案

前面我们提到了,使用 AOP 思想能够为我们解决日志、事物、权限等等的问题。现在有一个需求:为老的业务系统添加计算所有核心业务中方法执行的时间并记录日志。

public interface IInfoService {

  String addInfo(String info) throws Throwable;

  String updateInfo(String info);

  String delInfo(String info);

  String queryInfo(String info);
}

IInfoService 接口

public class InfoServiceImpl implements IInfoService {
@Override
public String addInfo(String info) { System.out.println("InfoServiceImpl.addInfo"); return "InfoServiceImpl.addInfo111";
} @Override
public String updateInfo(String info) { System.out.println("InfoServiceImpl.updateInfo"); return "InfoServiceImpl.updateInfo";
} @Override
public String delInfo(String info) { System.out.println("InfoServiceImpl.delInfo"); return "InfoServiceImpl.delInfo";
} @Override
public String queryInfo(String info) { System.out.println("InfoServiceImpl.queryInfo"); return "InfoServiceImpl.queryInfo";
}
}

InfoServiceImpl 实现类

IInfoService 接口,当中有 addInfo、updateInfo、delInfo、queryInfo 四个方法,InfoServiceImpl 类中实现了这四个方法,假如我们要统计 InfoServiceImpl 类中这四个方法具体的执行时间并为每个方法的执行情况都写入日志,我们应该如何来实现呢?

1、模板设计模式

我们设计了如下一个模板类:

public class ServiceTemplate {

  public static <T> T test(Supplier<T> supplier) {
// 1.记录开始时间并打印方法名,这里只能打印固定的方法名
long start = System.currentTimeMillis();
System.out.println("addInfo start..."); // 2.获取传入对象supplier的对象实例
T resp = supplier.get(); // 3.记录结束时间并打印方法名
long end = System.currentTimeMillis();
System.out.println("addInfo end...");
System.out.println("cost time:" + (end - start)); return resp;
}
}

我们使用了一个供给型(无参数,有返回值)的对象 supplier 作为参数传给静态方法 test(),在 supplier.get() 方法执行前和执行后分别记录时间,最后计算出方法执行的时间。

再来看看如何调用 InfoServiceImpl 类中的四个方法:

public class AppTemplate {

  public static void main(String[] args) {
  // 生成 Supplier 对象
Supplier<String> supplier = () -> {
try {
     IInfoService infoService = new InfoServiceImpl(); 
return infoService.addInfo("");
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return null;
}; /**
* 模板模式调用
* 调用 ServiceTemplate.test() 方法会执行supplier.get()方法
* 从而执行上面的infoService.addInfo("")
*/
String resp = ServiceTemplate.test(supplier); System.out.println(resp);
System.out.println("---------------模板调用模式---------------");
}
}

整个执行过程是这样的:首先会生成一个 Supplier 对象作为参数传给 ServiceTemplate.test() 方法,test() 方法执行时会先打印 "addInfo start..." 并记录方法开始执行的时间,然后调用 supplier.get() 方法时会执行 infoService.addInfo("") 方法,最后打印 "addInfo end..." 并计算方法执行的时间,运行结果如下:

但是,我们发现如果要执行其他方法时(比如updateInfo),把 infoService.addInfo("") 改为了 infoService.updateInfo(""),运行结果如下:

执行的方法变了,但是打印的开头日志和结尾的日志并没有变,因为我们在模板中日志的打印是写死的,没办法根据方法名而改变,因为在模板中调用哪个方法我们根本不知道,所以这种实现方式还是不完美。

2、装饰器设计模式

还是使用上面的 IInfoService 接口和 InfoServiceImpl 实现类。然后我们在设计一个 IInfoService 接口的实现类,如下:

public class DeclareInfoServiceImpl implements IInfoService {

  private IInfoService infoService;

  public DeclareInfoServiceImpl(IInfoService infoService) {
this.infoService = infoService;
} @Override
public String addInfo(String info) { System.out.println("addInfo start..."); String resp = null;
try {
resp = this.infoService.addInfo(info);
} catch (Throwable throwable) {
throwable.printStackTrace();
} System.out.println("addInfo end..."); return resp;
} @Override
public String updateInfo(String info) {
System.out.println("updateInfo start..."); String resp = this.infoService.updateInfo(info); System.out.println("updateInfo end..."); return resp;
} @Override
public String delInfo(String info) {
System.out.println("delInfo start..."); String resp = this.infoService.delInfo(info); System.out.println("delInfo end..."); return resp;
} @Override
public String queryInfo(String info) {
System.out.println("queryInfo start..."); String resp = this.infoService.queryInfo(info); System.out.println("queryInfo end..."); return resp;
}
}

DeclareInfoServiceImpl 实现类

该类实现自IInfoService 接口,构造方法参数为 IInfoService 对象,实现的所有方法除了必要的日志打印和时间计算外,再用IInfoService 对象调用一次同名的 addInfo、updateInfo 等方法,这样如果我们传入的是 InfoServiceImpl 类的对象,就能在执行方法的前后加上日志及计算时间。测试代码如下:

public class AppDeclare {

  public static void main(String[] args) throws Throwable {

    // 装饰器模式调用
IInfoService infoService = new DeclareInfoServiceImpl(new InfoServiceImpl());
infoService.addInfo("");
System.out.println("---------------装饰器调用模式---------------");
}
}

初看装饰器调用模式,发现很像 IO 文件操作体系中的缓冲流操作。没错,缓冲流操作也是用装饰器模式来设计的。下面来看下运行结果:

但是这种模式缺点也十分明显,我们需要对 DeclareInfoServiceImpl 类的所有实现方法都手动加上日志,如果方法非常多的情况下是不太可能的。

3、动态代理设计模式

动态代理模式使用的是JDK自带的类 Proxy 的 newProxyInstance() 方法生成一个代理对象,然后再用代理对象去执行对应的方法。

public class ServiceProxy {

  public static void main(String[] args) throws Throwable {

    IInfoService infoService = new InfoServiceImpl();

    /**
* 构建Proxy,生成代理对象proxyInfoService
* ClassLoader loader,
* Class<?>[] interfaces,
* InvocationHandler h
*/
IInfoService proxyInfoService = (IInfoService) Proxy.newProxyInstance(
// ClassLoader loader:类加载器
infoService.getClass().getClassLoader(),
// Class<?>[] interfaces:被代理对象的接口
infoService.getClass().getInterfaces(),
// InvocationHandler h:
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1.入口log
long start = System.currentTimeMillis();
System.out.println(String.format("%s start...", method.getName())); // 2. 执行业务代码
// 相当于是执行 InfoServiceImpl.addInfo方法的逻辑
Object response = method.invoke(infoService, args); // 3.出口log
long end = System.currentTimeMillis();
System.out.println(String.format("%s end...", method.getName()));
System.out.println("cost time:" + (end - start)); // 返回业务代码执行后的结果
return response;}}); // 通过代理对象调用具体的方法
proxyInfoService.addInfo("");
proxyInfoService.updateInfo("");
}
}

newProxyInstance() 方法有三个参数:

  • 第一个参数 ClassLoader 是类加载器,通过 infoService.getClass().getClassLoader() 来获取被代理对象 infoService 的类加载器;
  • 第二个参数 Class<?>[] interfaces 是被代理对象 infoService 所属接口的字节码对象数组,通过 infoService.getClass().getInterfaces() 来获取;
  • 第三个参数 InvocationHandler 对象重写了 InvocationHandler 类的 invoke() 方法,我们可以在里面增加时间统计、日志打印等功能。在 invoke() 方法内是通过 method.invoke() 方法执行被代理对象 infoService 对应的 addInfo()、updateInfo()等方法。

生成的代理对象 proxyInfoService 类似下面这个类的对象,只不过是虚拟的,我们看不到。

public class ProxyInfoServiceImpl implements IInfoService {

  private IInfoService infoService;

  private InvocationHandler h;

  public ProxyInfoServiceImpl(IInfoService infoService) {
this.infoService = infoService;
} @Override
public String addInfo(String info) throws Throwable { System.out.println("start"); String resp = infoService.addInfo(info); System.out.println("end"); // 大概模拟下InvocationHandler的调用过程
return (String) h.invoke(this.infoService, null, new Object[]{info});
} @Override
public String updateInfo(String info) {
return null;
} @Override
public String delInfo(String info) {
return null;
} @Override
public String queryInfo(String info) {
return null;
}
}

生成的虚拟代理类

这个类看上去很像装饰器模式的 DeclareInfoServiceImpl 实现类,它也是实现自接口 IInfoService,只不过在每个方法都会调一次 invike() 方法。

运行结果如下:

动态代理模式还可以使用第三方库 Cglib 来实现,它和上面使用 JDK 实现的动态代理的区别在于:

JDK 的动态代理是利用拦截器(必须实现InvocationHandler)加上反射机制生成一个代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理;

Cglib 动态代理是利用ASM框架,对代理对象类生成的class文件加载进来,通过修改其字节码生成子类来处理;

  • 目标对象生实现自接口,默认用JDK动态代理;
  • 如果目标对象使用了接口,可以强制使用 Cglib;
  • 如果目标对象没有实现接口,必须采用 Cglib 库,Spring会自动在 JDK 动态代理和 Cglib 之间转换;

三、使用 XML 文件配置AOP

还是先创建一个接口和实现类

public interface IInfoService {

  String addInfo(String info) throws Throwable;

  String updateInfo(String info);

  String delInfo(String info);

  String queryInfo(String info);
}

IInfoService 接口

public class InfoServiceImpl implements IInfoService {
@Override
public String addInfo(String info) { System.out.println("InfoServiceImpl.addInfo"); return "InfoServiceImpl.addInfo";
} @Override
public String updateInfo(String info) { System.out.println("InfoServiceImpl.updateInfo"); return "InfoServiceImpl.updateInfo";
} @Override
public String delInfo(String info) { System.out.println("InfoServiceImpl.delInfo"); return "InfoServiceImpl.delInfo";
} @Override
public String queryInfo(String info) { System.out.println("InfoServiceImpl.queryInfo"); return "InfoServiceImpl.queryInfo";
}
}

InfoServiceImpl 实现类

再创建一个切面类

public class PrintLogHandler {

  /**
* 入口log,通知: 前置通知
*/
public void preLog() {
System.out.println("before log start");
} /**
* 出口log
*/
public void postLog() {
System.out.println("after log start");
} /**
* 异常通知
*/
public void errLog() {
System.err.println("哈哈 出异常啦");
} /**
* 最终通知
*/
public void finalLog() {
System.out.println("最终通知");
} /**
* 环绕通知
*/
public Object aroundTest(ProceedingJoinPoint joinPoint) {
try {
System.out.println("around before log"); Object response = joinPoint.proceed(); System.out.println("around after log"); return response;
} catch (Throwable t) {
System.out.println("around exception log");
} finally {
System.out.println("around finally log");
}
return null;
}
}

PrintLogHandler 切面类

前面类当中定义了前置通知、后置通知、异常通知、最终通知和环绕通知,他们的处理逻辑和 try..catch..finally是一样的。另外,我们还需要定义一个 XML 配置文件,如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="infoserver" class="com.jack.course.spring.aopx.service.impl.InfoServiceImpl"/>
<bean id="msgserver" class="com.jack.course.spring.aopx.service.impl.MsgServiceImpl"/>
<bean id="logHandler" class="com.jack.course.spring.aopx.hander.PrintLogHandler"/> <!-- 开始aop配置 -->
<aop:config>
<!-- 切面 -->
<aop:aspect ref="logHandler">
<!-- 切入点 -->
<!-- expression:切入点表达式 -->
<aop:pointcut id="pp"
expression="execution(* com.jack.course.spring.aopx.service.impl.*.*(..))"/>
<!-- 前置通知 -->
<aop:before method="preLog" pointcut-ref="pp"/>
<!-- 后置通知-->
<aop:after-returning method="postLog" pointcut-ref="pp"/>
<!-- 异常通知 -->
<aop:after-throwing method="errLog" pointcut-ref="pp"/>
<!-- 最终通知-->
<aop:after method="finalLog" pointcut-ref="pp"/>
<!-- 环绕通知,相当于上面几种通知的结合体-->
<aop:around method="aroundTest" pointcut-ref="pp"/>
</aop:aspect>
</aop:config>
</beans>
public class App {

  public static void main(String[] args) throws Throwable {

    ApplicationContext context = new ClassPathXmlApplicationContext("aopx/beans.xml");
IInfoService infoService = context.getBean(IInfoService.class); infoService.addInfo("hello");
infoService.updateInfo("hello");
}
}

测试代码

这里,我们简单介绍下切入点表达式

  • 示例: execution(* com.jack.course.spring.aopx.service.impl.*.*(..))
  • execution + (表达式)
    • (权限修饰符) (返回值) (包名).(类).(方法)(参数列表)
    • * 表示通配符
    • (..) 表示匹配所有的参数

四、使用注解配置AOP

使用全注解的方式,首先需要增加一个配置类

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.jack.course.spring.aopa")
public class AopaConfiguration { }

AopaConfiguration 配置类

另外,切面类需要加上对应的注解

@Aspect
@Component
public class LogHandler { @Pointcut("execution(* com.jack.course.spring.aopa.service.impl.*.*(..))")
public void pp(){
} /**
* 入口log,通知: 前置通知
*/
@Before("pp()")
public void preLog() {
System.out.println("注解版:before log start");
} /**
* 出口log
*/
@AfterReturning("pp()")
public void postLog() {
System.out.println("注解版:after log start");
} /**
* 异常通知
*/
@AfterThrowing("pp()")
public void errLog() {
System.err.println("注解版:哈哈 出异常啦");
} /**
* 最终通知
*/
@After("pp()")
public void finalLog() {
System.out.println("注解版:最终通知");
} /**
* 环绕通知
*/
@Around("pp()")
public Object aroundTest(ProceedingJoinPoint joinPoint) {
try {
System.out.println("around before log"); Object response = joinPoint.proceed(); System.out.println("around after log");
return response;
} catch (Throwable t) {
System.out.println("around exception log");
} finally {
System.out.println("around finally log");
}
return null;
}
}

LogHandler 切面类

实现类需要加上注解 @Service

@Service
public class InfoServiceImpl implements IInfoService {
@Override
public String addInfo(String info) { System.out.println("InfoServiceImpl.addInfo"); return "InfoServiceImpl.addInfo";
} @Override
public String updateInfo(String info) { System.out.println("InfoServiceImpl.updateInfo"); return "InfoServiceImpl.updateInfo";
} @Override
public String delInfo(String info) { System.out.println("InfoServiceImpl.delInfo"); return "InfoServiceImpl.delInfo";
} @Override
public String queryInfo(String info) { System.out.println("InfoServiceImpl.queryInfo"); return "InfoServiceImpl.queryInfo";
}
}

InfoServiceImpl 实现类

public class App {

  public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AopaConfiguration.class);
IMsgService msgService = context.getBean(IMsgService.class);
msgService.addMsg("");
msgService.delMsg("");
}
}

测试代码

通知执行顺序

常用注解说明:

  • @Configuration

    • 指定当前类是一个配置类,它的作用和bean.xml一样
  • @ComponentScan
    • 用于通过注解指定spring在创建容器时要扫描的包
  • @EnableAspectJAutoProxy
    • 开启自动切面的支持
  • @Aspect
    • 把当前类标识为一个切面供容器读取
  • @Pointcut
    • 切入点,根据表达式确定被切面切入的包、类或者方法;
  • @Before
    • 前置通知,在方法执行前执行
  • @AfterReturning
    • 后置通知,方法正常退出后执行
  • @AfterThrowing
    • 异常通知,方法抛出异常时执行
  • @After
    • 最终通知,不管方法是正常退出还是抛出异常都会执行
  • @Around
    • 环绕通知,围绕着方法执行

Spring学习之==>AOP的更多相关文章

  1. Spring学习之AOP的实现方式

    Spring学习之AOP的三种实现方式 一.介绍AOP 在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能 ...

  2. Spring学习之AOP总结帖

    AOP(面向方面编程),也可称为面向切面编程,是一种编程范式,提供从另一个角度来考虑程序结构从而完善面向对象编程(OOP). 在进行 OOP 开发时,都是基于对组件(比如类)进行开发,然后对组件进行组 ...

  3. spring学习(二) ———— AOP之AspectJ框架的使用

    前面讲解了spring的特性之一,IOC(控制反转),因为有了IOC,所以我们都不需要自己new对象了,想要什么,spring就给什么.而今天要学习spring的第二个重点,AOP.一篇讲解不完,所以 ...

  4. Spring学习之AOP

    Spring-AOP(Aspect-orented programming) 在业务流程中插入与业务无关的逻辑,这样的逻辑称为Cross-cutting concerns,将Crossing-cutt ...

  5. Spring学习之Aop的各种增强方法

    AspectJ允许使用注解用于定义切面.切入点和增强处理,而Spring框架则可以识别并根据这些注解来生成AOP代理.Spring只是使用了和AspectJ 5一样的注解,但并没有使用AspectJ的 ...

  6. Spring学习之Aop的基本概念

    转自:http://my.oschina.net/itblog/blog/209067 AOP的基本概念 AOP从运行的角度考虑程序的流程,提取业务处理过程的切面.AOP面向的是程序运行中的各个步骤, ...

  7. Spring学习之AOP与事务

      一.概述 在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术.AOP是OOP的延续, ...

  8. spring学习笔记-AOP

    1.aop:aspect oriented programming 面向切面编程 2.aop在spring中的作用:   提供声明式服务(声明式事务) 允许用户实现自定义切面 3.aop:在不改变原有 ...

  9. Spring 学习之AOP

    1. 走进面前切面编程 编程范式: 面向过程编程,c语言: 面向对象编程:c++,java,c#; 函数式编程: 事件驱动编程: 面向切面编程: AOP是一种编程范式,不是编程语言:解决特定问题,不能 ...

  10. Spring 学习二-----AOP的原理与简单实践

    一.Spring  AOP的原理 AOP全名Aspect-Oriented Programming,中文直译为面向切面(方面)编程.何为切面,就比如说我们系统中的权限管理,日志,事务等我们都可以将其看 ...

随机推荐

  1. 从n个数里面找最大的两个数理论最少需要比较

    答案是:n+logn-2 过程是这样的: 甲乙比甲胜出,丙丁比丙胜出,最后甲丙比较,甲胜出...容易得出找出最大数为n-1次. 现在开始找出第二大的数字:明显,第二大的数字,一定和甲进行过比较.... ...

  2. ubantu32位 linux下hexedit的下载安装

    Hexedit软件介绍: hexedit是一个开源的完全免费的命令行软件,可用于在任何GNU / Linux操作系统下以十六进制和ASCII(美国信息交换标准代码)格式查看和编辑文件. 下载: 在so ...

  3. [uboot] (番外篇)uboot串口&console&stdio设备工作流程 (转)

    [uboot] uboot流程系列:[project X] tiny210(s5pv210)上电启动流程(BL0-BL2)[project X] tiny210(s5pv210)从存储设备加载代码到D ...

  4. regex正则

    1 正则表达式基本语法 两个特殊的符号^和$.他们的作用是分别指出一个字符串的开始和结束.例子如下: ^The:表示所有以”The”开始的字符串(”There”,”The cat”等): of des ...

  5. 去除IntelliJ IDEA中重复代码报灰黄色的下划波浪线

    最近写Java在用IntelliJ IDEA这款传说中的神器IDE,看群里的大神们都在用,也耐不住寂寞想向大神们看齐一下.刚开始用,很多地方也不是很熟,今天遇到一个问题,导入一个项目后,看有些类里的代 ...

  6. Storm实践(一):基础知识

    storm简介 Storm是一个分布式实时流式计算平台,支持水平扩展,通过追加机器就能提供并发数进而提高处理能力:同时具备自动容错机制,能自动处理进程.机器.网络等异常. 它可以很方便地对流式数据进行 ...

  7. Vue数据通信详解

    如果有需要源代码,请猛戳源代码 希望文章给大家些许帮助和启发,麻烦大家在GitHub上面点个赞!!!十分感谢 一.前言 组件是 vue.js最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着 ...

  8. 2019CCPC秦皇岛赛区(重现赛)- F

    链接: http://acm.hdu.edu.cn/contests/contest_showproblem.php?pid=1006&cid=872 题意: Z 国近年来一直在考虑遏制国土沙 ...

  9. 2 zabbix安装与部署

    官方文档:https://www.zabbix.com/documentation/3.0/manual 中文文档  https://www.zabbix.com/documentation/3.4/ ...

  10. Linux下使用telnet测试端口号是否开放

    telnet 127.0.0.1 80调用后,若提示bash: telnet: command not found,那么进行以下步骤: 1.检查telnet是否已经安装,或者有部分未安装: rpm - ...