本节主要内容:
一、Spring 通过XML配置文件形式来AOP 来实现前置,环绕,异常通知
1. Spring AOP 前置通知 XML配置使用案例
2. Spring AOP 环绕通知 XML配置使用案例
3. Spring AOP 抛出异常后通知 XML配置使用案例
4. Spring AOP 返回后通知 XML配置使用案例
5. Spring AOP 后通知 XML配置使用案例
二、Spring 通过注解形式来AOP 来实现前置,环绕,异常通知
1. Spring AOP 前置通知 注解使用案例
2. Spring AOP 环绕通知 注解使用案例
3. Spring AOP 抛出异常后通知 注解使用案例
4. Spring AOP 返回后通知 注解使用案例
5. Spring AOP 后通知 注解使用案例
AOP是Aspect Oriented Programming的缩写,意思是面向方面编程,AOP实际是GoF设计模式的延续
关于Spring AOP的一些术语
- 切面(Aspect):在Spring AOP中,切面可以使用通用类或者在普通类中以@Aspect 注解(@AspectJ风格)来实现
- 连接点(Joinpoint):在Spring AOP中一个连接点代表一个方法的执行
- 通知(Advice):在切面的某个特定的连接点(Joinpoint)上执行的动作。通知有各种类型,其中包括"around"、"before”和"after"等通知。许多AOP框架,包括Spring,都是以拦截器做通知模型, 并维护一个以连接点为中心的拦截器链
- 切入点(Pointcut):定义出一个或一组方法,当执行这些方法时可产生通知,Spring缺省使用AspectJ切入点语法。
通知类型
- 前置通知(@Before):在某连接点(join point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)
- 返回后通知(@AfterReturning):在某连接点(join point)正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回
- 抛出异常后通知(@AfterThrowing):方法抛出异常退出时执行的通知
- 后通知(@After):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)
- 环绕通知(@Around):包围一个连接点(join point)的通知,如方法调用。这是最强大的一种通知类型,环绕通知可以在方法调用前后完成自定义的行为,它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行
Spring 实现AOP是依赖JDK动态代理和CGLIB代理实现的。以下是JDK动态代理和CGLIB代理简单介绍
JDK动态代理:其代理对象必须是某个接口的实现,它是通过在运行期间创建一个接口的实现类来完成对目标对象的代理。 CGLIB代理:实现原理类似于JDK动态代理,只是它在运行期间生成的代理对象是针对目标类扩展的子类。CGLIB是高效的代码生成包,底层是依靠ASM(开源的java字节码编辑类库)操作字节码实现的,性能比JDK强。
在Spring中,有接口时将采用JDK的方式实现proxy代理对象,当没有接口时,将采用cglib中的方式实现prixy代理对象。
一、 Spring 通过XML配置文件形式来AOP 来实现前置,环绕,异常通知
1 Spring AOP前置通知案例
1.1 问题
使用Spring AOP前置通知,在访问Controller中每个方法前,记录用户的操作日志。
1.2 方案
Spring AOP使用步骤:
1.3 步骤
实现此案例需要按照如下步骤进行。
步骤一:创建Controller,创建新项目SpringAOP。
导入Spring 环境的jar包 :
如果没有jar包,那么可以上去上面下一个。下载地址:http://yunpan.cn/cdXTcJtZfJqQk 访问密码 6c96
创建员工业务控制器EmpController,并实现员工查询,代码如下:
代码语言:javascript复制package com.souvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 控制类
*
*/
@Controller
@RequestMapping("/emp")
public class EmpController {
/**
* 方法名:find</br>
* 详述: 查询员工 </br>
* 开发人员:hgf </br>
* 创建时间:2019年08月19日 </br>
* @param userid
* @param password
* @return
* @throws
*/
@RequestMapping("/findEmp.do")
public String find(String userid,String password) {
// 模拟查询员工数据
System.out.println("执行,find()方法,查询员工数据,发送至列表页面.");
return "emp/emp_list.jsp";
}
}
步骤二:创建方面组件
创建方面组件OperateLogger,并在该类中创建记录用户操作日志的方法,代码如下:
代码语言:javascript复制package com.souvc.aspect;
/**
* 用于记录日志的方面组件,演示Spring AOP的各种通知类型。
*/
public class OperateLogger {
/**
* 方法名:log1</br>
* 详述:测试前置通知 </br>
* 开发人员:hgf </br>
* 创建时间:2019年08月19日 </br>
* @throws
*/
public void log1() {
// 记录日志
System.out.println("进入log1()方法");
}
}
步骤三:声明方面组件
在applicationContext.xml中,声明该方面组件,关键代码如下:
代码语言:javascript复制
<!-- 声明方面组件 -->
<bean id="operateLogger" class="com.souvc.aspect.OperateLogger"/>
步骤四:将方面组件作用到目标组件上
在applicationContext.xml中,将声明的方面组件作用到com.souvc.controller包下所有类的所有方法上,关键代码如下:
代码语言:javascript复制 <!-- 声明方面组件 -->
<bean id="operateLogger" class="com.souvc.aspect.OperateLogger"/>
<!-- 配置AOP -->
<aop:config>
<aop:aspect ref="operateLogger">
<aop:before method="log1" pointcut="within(com.souvc.controller..*)"/>
</aop:aspect>
</aop:config>
步骤五:测试
创建Junit测试类TestEmpController,并增加测试查询员工的方法,代码如下:
代码语言:javascript复制package com.souvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 控制类
*
*/
@Controller
@RequestMapping("/emp")
public class EmpController {
/**
* 方法名:find</br>
* 详述: 查询员工 </br>
* 开发人员:hgf </br>
* 创建时间:2019年08月19日 </br>
* @param userid
* @param password
* @return
* @throws
*/
@RequestMapping("/findEmp.do")
public String find(String userid,String password) {
// 模拟查询员工数据
System.out.println("执行,find()方法,查询员工数据,发送至列表页面.");
return "emp/emp_list.jsp";
}
}
执行该测试方法,控制台输出效果:
代码语言:javascript复制进入log1()方法
执行,find()方法,查询员工数据,发送至列表页面.
可见,在执行EmpController.find()方法之前,执行了方面组件的记录日志的方法,由于该方法采用AOP面向对象的思想实现的,因此不需要对Controller类做任何改动。
2 Spring AOP环绕通知案例
2.1 问题
使用Spring AOP环绕通知,在访问Controller中每个方法前,记录用户的操作日志。
2.2 方案
Spring AOP使用步骤:
2.3 步骤
实现此案例需要按照如下步骤进行。
步骤一:创建方面组件
复用方面组件OperateLogger,在该类中创建新的记录日志的方法log2,代码如下:
代码语言:javascript复制/**
* 方法名:log2</br>
* 详述:环绕通知使用的方法 </br>
* 开发人员:hgf </br>
* 创建时间:2019年08月19日 </br>
* @param p
* @return
* @throws Throwable
* @throws
*/
public Object log2(ProceedingJoinPoint p) throws Throwable {
// 目标组件的类名
String className = p.getTarget().getClass().getName();
// 调用的方法名
String method = p.getSignature().getName();
// 当前系统时间
String date = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss") .format(new Date());
// 拼日志信息
String msg = "-->用户在" date ",执行了" className "." method "()";
// 记录日志
System.out.println(msg);
// 执行目标组件的方法
Object obj = p.proceed();
// 在调用目标组件业务方法后也可以做一些业务处理
System.out.println("-->调用目标组件业务方法后...");
return obj;
}
步骤二:声明方面组件
由于复用的方面组件已经声明,因此该步骤可以省略。
步骤三:将方面组件作用到目标组件上
在applicationContext.xml中,声明方面组件的log2方法,关键代码如下:
代码语言:javascript复制<aop:aspect ref="operateLogger">
<aop:around method="log2" pointcut="within(com.souvc.controller..*)"/>
</aop:aspect>
步骤四:测试
执行测试方法TestEmpController.test1(),控制台输出效果如下:
代码语言:javascript复制进入log1()方法
-->用户在2019-008-19 17:06:54,执行了com.souvc.controller.EmpController.find()
执行,find()方法,查询员工数据,发送至列表页面.
-->调用目标组件业务方法后..
项目详细代码:
applicationContext.xml
代码语言:javascript复制<?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:context="http://www.springframework.org/schema/context"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.2.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd">
<!-- 开启注解扫描 -->
<context:component-scan base-package="com.souvc" />
<!-- 支持@RequestMapping请求和Controller映射 -->
<mvc:annotation-driven />
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/" />
<property name="suffix" value=".jsp" />
</bean>
<!-- 声明方面组件-->
<bean id="operateLogger" class="com.souvc.aspect.OperateLogger" />
<!-- 配置AOP -->
<aop:config>
<!-- 测试前置通知 -->
<aop:aspect ref="operateLogger">
<aop:before method="log1" pointcut="within(com.souvc.controller..*)"/>
</aop:aspect>
<!-- 测试环绕通知 -->
<aop:aspect ref="operateLogger">
<aop:around method="log2" pointcut="within(com.souvc.controller..*)"/>
</aop:aspect>
</aop:config>
</beans>
OperateLogger.java
代码语言:javascript复制package com.souvc.aspect;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* 用于记录日志的方面组件,演示Spring AOP的各种通知类型。
*/
public class OperateLogger {
/**
* 方法名:log1</br>
* 详述:测试前置通知 </br>
* 开发人员:hgf </br>
* 创建时间:2019年08月19日 </br>
* @throws
*/
public void log1() {
// 记录日志
System.out.println("进入log1()方法");
}
/**
* 方法名:log2</br>
* 详述:环绕通知使用的方法 </br>
* 开发人员:hgf </br>
* 创建时间:2019年08月19日 </br>
* @param p
* @return
* @throws Throwable
* @throws
*/
public Object log2(ProceedingJoinPoint p) throws Throwable {
// 目标组件的类名
String className = p.getTarget().getClass().getName();
// 调用的方法名
String method = p.getSignature().getName();
// 当前系统时间
String date = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss") .format(new Date());
// 拼日志信息
String msg = "-->用户在" date ",执行了" className "." method "()";
// 记录日志
System.out.println(msg);
// 执行目标组件的方法
Object obj = p.proceed();
// 在调用目标组件业务方法后也可以做一些业务处理
System.out.println("-->调用目标组件业务方法后...");
return obj;
}
}
3 Spring AOP异常通知案例
3.1 问题
使用Spring AOP异常通知,在每个Controller的方法发生异常时,记录异常日志。
3.2 方案
Spring AOP使用步骤:
3.3 步骤
实现此案例需要按照如下步骤进行。
步骤一:创建方面组件
复用方面组件OperateLogger,在该类中创建新的记录日志的方法log3,代码如下:
代码语言:javascript复制 /**
* 方法名:log3</br>
* 详述:测试异常通知使用的方法</br>
* 开发人员:hgf </br>
* 创建时间:2019年08月19日 </br>
* @param e
* @throws
*/
public void log3(Exception e) {
StackTraceElement[] elems = e.getStackTrace();
// 将异常信息记录
System.out.println("-->" elems[0].toString());
}
步骤二:声明方面组件
由于复用的方面组件已经声明,因此该步骤可以省略。
步骤三:将方面组件作用到目标组件上
在applicationContext.xml中,声明方面组件的log3方法,关键代码如下:
代码语言:javascript复制 <aop:aspect ref="operateLogger">
<aop:after-throwing method="log3" throwing="e" pointcut="within(com.souvc.controller..*)"/>
</aop:aspect>
关键配置代码:
代码语言:javascript复制<!-- 开启注解扫描 -->
<context:component-scan base-package="com.souvc" />
<!-- 支持@RequestMapping请求和Controller映射 -->
<mvc:annotation-driven />
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/" />
<property name="suffix" value=".jsp" />
</bean>
<!-- 声明方面组件-->
<bean id="operateLogger" class="com.souvc.aspect.OperateLogger" />
<!-- 配置AOP -->
<aop:config>
<!-- 测试前置通知 -->
<aop:aspect ref="operateLogger">
<aop:before method="log1" pointcut="within(com.souvc.controller..*)"/>
</aop:aspect>
<!-- 测试环绕通知 -->
<aop:aspect ref="operateLogger">
<aop:around method="log2" pointcut="within(com.souvc.controller..*)"/>
</aop:aspect>
<!-- 测试异常通知 -->
<aop:aspect ref="operateLogger">
<aop:after-throwing method="log3" throwing="e" pointcut="within(com.souvc.controller..*)"/>
</aop:aspect>
</aop:config>
步骤四:测试
为了便于测试,在EmpController.find()方法中制造一个异常,代码如下:
主要代码:
代码语言:javascript复制 // 制造一个异常,便于测试异常通知
//Integer.valueOf("abc");
五、测试效果
代码语言:javascript复制进入log1()方法
-->用户在2019-08-19 17:12:07,执行了com.souvc.controller.EmpController.find()
执行,find()方法,查询员工数据,发送至列表页面.
-->java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
4 Spring AOP 后返回通知案例
4.1 问题
使用Spring AOP后返回通知类型。
4.2 方案
Spring AOP使用步骤:
4.3 步骤
实现此案例需要按照如下步骤进行。
5 Spring AOP 执行后案例
5.1 问题
使用Spring AOP 执行后通知类型。
5.2 方案
Spring AOP使用步骤:
5.3 步骤
实现此案例需要按照如下步骤进行。
步骤一:
在 OperateLogger.java 添加以下代码:
代码语言:javascript复制 /**
* 方法名:log5</br>
* 详述:测试 执行后使用的方法</br>
* 开发人员:hgf </br>
* 创建时间:2019年08月19日 </br>
* @param result
* @return
* @throws
*/
public void log5() {
// 记录日志
System.out.println("进入log5()方法");
}
步骤二:
在 applicationContext.xml 添加以下代码:
代码语言:javascript复制
<!-- 测试后通知 -->
<aop:aspect ref="operateLogger">
<aop:after method="log5" pointcut="within(com.souvc.controller..*)"/>
</aop:aspect>
步骤三:
运行测试类
步骤四:
效果如下:
代码语言:javascript复制进入log1()方法
-->用户在2019-08-19 17:53:10,执行了com.souvc.controller.EmpController.find()
执行,find()方法,查询员工数据,发送至列表页面.
进入log5()方法
-->java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
二、 Spring 通过注解形式形式来AOP 来实现前置,环绕,异常通知
Spring AOP相关注解及含义如下:
@Aspect:用于声明方面组件
@Before:用于声明前置通知
@AfterReturning:用于声明后置通知
@After:用于声明最终通知
@Around:用于声明环绕通知
@AfterThrowing:用于声明异常通知
1 Spring AOP注解使用案例
1.1 问题
使用Spring AOP注解替代XML配置,重构上面的3个案例。
1.2 方案
分别在对应的方法上面加上注解。
1.3 步骤
实现此案例需要按照如下步骤进行。
步骤一:开启AOP注解扫描
在applicationContext.xml中,去掉方面组件声明及作用的XML配置,并开启AOP注解扫描,关键代码如下:
代码语言:javascript复制<!-- 声明方面组件 -->
<!-- <bean id="operateLogger" class="com.souvc.aspect.OperateLogger" />-->
<!-- 配置AOP -->
<!-- <aop:config>
<aop:aspect ref="operateLogger">
<aop:before method="log1" pointcut="within(com.souvc.controller..*)"/>
</aop:aspect>
<aop:aspect ref="operateLogger">
<aop:around method="log2" pointcut="within(com.souvc.controller..*)"/>
</aop:aspect>
<aop:aspect ref="operateLogger">
<aop:after-throwing method="log3" throwing="e" pointcut="within(com.souvc.controller..*)"/>
</aop:aspect>
</aop:config> -->
代码语言:javascript复制<!-- 开启AOP注解扫描 -->
<aop:aspectj-autoproxy proxy-target-class="true"/>
或者:
代码语言:javascript复制<!-- 启用spring对AspectJ注解的支持 -->
<aop:aspectj-autoproxy/>
步骤二:使用注解声明方面组件
在OperateLogger中,使用@Aspect注解声明方面组件,并分别用@Before、@Around、@AfterThrowing注解声明log1、log2、log3方法,将方面组件作用到目标组件上,代码如下:
代码语言:javascript复制package com.souvc.aspect;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
/**
* 用于记录日志的方面组件,演示Spring AOP的各种通知类型。
*/
@Component
@Aspect
public class OperateLogger {
/**
* 方法名:log1</br>
* 详述:测试前置通知 </br>
* 开发人员:hgf </br>
* 创建时间:2019年08月19日 </br>
* @throws
*/
@Before("within(com.souvc.controller..*)")
public void log1() {
// 记录日志
System.out.println("进入log1()方法");
}
/**
* 方法名:log2</br>
* 详述:环绕通知使用的方法 </br>
* 开发人员:hgf </br>
* 创建时间:2019年08月19日 </br>
* @param p
* @return
* @throws Throwable
* @throws
*/
@Around("within(com.souvc.controller..*)")
public Object log2(ProceedingJoinPoint p) throws Throwable {
// 目标组件的类名
String className = p.getTarget().getClass().getName();
// 调用的方法名
String method = p.getSignature().getName();
// 当前系统时间
String date = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss") .format(new Date());
// 拼日志信息
String msg = "-->用户在" date ",执行了" className "." method "()";
// 记录日志
System.out.println(msg);
// 执行目标组件的方法
Object obj = p.proceed();
// 在调用目标组件业务方法后也可以做一些业务处理
System.out.println("-->调用目标组件业务方法后...");
return obj;
}
/**
* 方法名:log3</br>
* 详述:测试异常通知使用的方法</br>
* 开发人员:hgf </br>
* 创建时间:2019年08月19日 </br>
* @param e
* @throws
*/
@AfterThrowing(pointcut = "within(com.souvc.controller..*)", throwing = "e")
public void log3(Exception e) {
StackTraceElement[] elems = e.getStackTrace();
// 将异常信息记录
System.out.println("-->" elems[0].toString());
}
/**
* 方法名:log4</br>
* 详述:测试 返回后通知使用的方法</br>
* 开发人员:hgf </br>
* 创建时间:2019年08月19日 </br>
* @param result
* @return
* @throws
*/
@AfterReturning(value="execution(* com.souvc.controller.*.*(..))",returning="result")
public void log4(JoinPoint joinPoint,Object result ){
Object object = joinPoint.getSignature();//方法返回值
System.out.println("joinPoint.getKind():" joinPoint.getKind());
System.out.println("joinPoint.getTarget():" joinPoint.getTarget());
System.out.println("joinPoint.getThis():" joinPoint.getThis());
System.out.println("joinPoint.getArgs():" joinPoint.getArgs().length);
Object [] args=joinPoint.getArgs();
for (int i = 0; i < args.length; i ) {
System.out.println("参数:" args[i]);
}
System.out.println("joinPoint.getSignature():" joinPoint.getSignature());
System.out.println("joinPoint.getSourceLocation():" joinPoint.getSourceLocation());
System.out.println("joinPoint.getStaticPart():" joinPoint.getStaticPart());
String rightnow=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date());
System.out.println(rightnow "执行了【" object "方法正常执行结束......】" "【返回结果:" result "】");
}
/**
* 方法名:log5</br>
* 详述:测试 执行后使用的方法</br>
* 开发人员:hgf </br>
* 创建时间:2019年08月19日 </br>
* @param result
* @return
* @throws
*/
// @After(value="execution(com.souvc.controller.*.*(..))")
// public void log5() {
// // 记录日志
// System.out.println("进入log5()方法");
// }
}
步骤三:测试
执行测试方法TestEmpController.test1(),结果如下:
无异常的时候:
代码语言:javascript复制-->用户在2019-08-19 18:30:22,执行了com.souvc.controller.EmpController.find()
-->记录用户操作信息
查询员工数据,发送至列表页面.
-->调用目标组件业务方法后...
有异常的时候:
代码语言:javascript复制-->用户在2019-08-19 18:32:27,执行了com.souvc.controller.EmpController.find()
-->记录用户操作信息
查询员工数据,发送至列表页面.
-->java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
1)JoinPoint
java.lang.Object[] getArgs():获取连接点方法运行时的入参列表; Signature getSignature() :获取连接点的方法签名对象; java.lang.Object getTarget() :获取连接点所在的目标对象; java.lang.Object getThis() :获取代理对象本身;
2)ProceedingJoinPoint
ProceedingJoinPoint继承JoinPoint子接口,它新增了两个用于执行连接点方法的方法: java.lang.Object proceed() throws java.lang.Throwable:通过反射执行目标对象的连接点处的方法; java.lang.Object proceed(java.lang.Object[] args) throws java.lang.Throwable:通过反射执行目标对象连接点处的方法,不过使用新的入参替换原来的入参。