Day5 Spring AOP
Spring AOP
AOP 相关术语
Spring AOP是对方法的增强。
-
连接点(JoinPoint)
方法 -
切入点(Pointcut)
确定哪些方法需要增强 -
通知(Advice)
方法运行的哪一个阶段,开始时、结束时、返回时、异常时、开始+结束时?
做了什么增强,添加日志、事务、负载均衡? -
切面(Aspect)
在哪里做了哪些增强,即切面=切入点+通知
面向切面编程要做什么?
在哪些类的哪些方法,在方法运行到什么时候,做哪些增强;
在哪些类的哪些方法 = 切入点(Pointcut)
在方法运行到什么时候 + 做哪些增强 = 通知(Advice)
使用方法
基于接口
Spring AOP最早完全是几个接口来完成的。
advice
Spring AOP最早完全是几个接口来完成的。
- 通过实现
AfterReturningAdvice
接口,定义一个后置通知(被代理方法运行结束后,该通知被调用);
public class LogAdvice implements AfterReturningAdvice {
private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println(dateFormat.format(new Date()) + ": method " + method.getName() + " invoked.");
}
}
- 配置xml文件
重点是配置代理工厂:ProxyFactoryBean
<!--被代理bean-->
<bean id="userService" class="com.bailiban.day4.aop.dynamic_proxy.noproxy.UserServiceImpl" />
<!-- 代理通知bean -->
<bean id="logAdvice" class="com.bailiban.day4.aop.spring12.LogAdvice" />
<!-- 代理工厂bean -->
<bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 被代理接口-->
<property name="proxyInterfaces">
<list>
<value>com.bailiban.day4.aop.dynamic_proxy.noproxy.UserService</value>
</list>
</property>
<!-- 被代理bean-->
<property name="target" ref="userService"/>
<!-- 通知-->
<property name="interceptorNames">
<list>
<value>logAdvice</value>
</list>
</property>
</bean>
从userServiceProxy的配置可以看出,其配置与手动编写基于JDK的动态代理很类似,都设计三个方面:
- 被代理接口:UserService
- 被代理类:UserServiceImpl
- 增强方法:logAdvice
测试:
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
"com/bailiban/day4/aop/spring12/advice.xml");
UserService userService = (UserService) context.getBean("userServiceProxy");
userService.getUser();
userService.createUser();
userService.updateUser();
userService.deleteUser();
}
结果与手动编写动态代理代码一致:
getUser
2019-12-22 14:50:20: method getUser invoked.
createUser
2019-12-22 14:50:20: method createUser invoked.
updateUser
2019-12-22 14:50:20: method updateUser invoked.
deleteUser
2019-12-22 14:50:20: method deleteUser invoked.
基于接口还可以有其他更复杂的配置,来提供更丰富的AOP编程。不过现在很少使用基于接口来编写Spring AOP,在此略过。
其他用法可参考代码:https://github.com/Jun67/spring-demo/tree/master/src/main/java/com/bailiban/day4/aop/spring12
advisor
advice默认对指定代理对象的所有方法做增强,使用advisor可以进一步指定增强哪些方法。
- 新增一个advisor bean
<!-- 在匹配(按方法名)的方法上通知-->
<bean id="advisor" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
<!-- 1. 通知做哪些事情-->
<property name="advice" ref="logAdvice" />
<!-- 2. 指定哪些方法-->
<property name="mappedNames" value="getUser" />
</bean>
- 修改ProxyFactoryBean拦截器配置,添加advisor
<!-- 动态代理-->
<bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 指定接口-->
<property name="proxyInterfaces" value="com.bailiban.day5.aop.noproxy.UserService" />
<!-- 指定代理对象-->
<property name="target" ref="userService" />
<!-- 指定增强的方法 -->
<property name="interceptorNames">
<list>
<!-- <value>beforeAdvice</value>-->
<!-- <value>logAdvice</value>-->
<!-- 加入新增的advisor,用于控制指定方法做增强处理-->
<value>advisor</value>
</list>
</property>
</bean>
- 测试
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
"com/bailiban/day5/aop/springaop/bean/advisor/advisor.xml");
UserService userService = (UserService) context.getBean("userServiceProxy");
userService.createUser();
userService.deleteUser();
userService.updateUser();
userService.getUser();
}
- 运行结果
创建了一个新用户。
删除一个用户。
更新一个用户。
查找一个用户。
2019-12-25 09:02:55: method getUser invoked.
autoProxyCreator
advice和advisor,在使用代理bean的时候,需要通过代理bean(userServiceProxy)来获取原bean名称(userService)。
可以是autoProxyCreator来自定生成代理类。在使用时,直接访问原bean名称(userService)即可获取代理对象。
- 创建了一个新的代理bean
<!-- 动态代理,自动生成代理对象-->
<bean id="userServiceProxy" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<!-- 按bean的名称来进行代理,以Service结尾的bean都会被代理-->
<property name="beanNames" value="*Service" />
<!-- 指定增强的方法 -->
<property name="interceptorNames">
<list>
<!-- advisor,用于控制指定方法做增强处理-->
<value>advisor</value>
</list>
</property>
</bean>
- 测试
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
"com/bailiban/day4/aop/spring12/autoproxy/autoproxy.xml");
UserService userService = (UserService) context.getBean("userService");
userService.getUser();
userService.createUser();
userService.updateUser();
userService.deleteUser();
}
结果:
getUser
2019-12-25 17:55:43: method getUser invoked.
createUser
2019-12-25 17:55:43: method createUser invoked.
updateUser
deleteUser
DafaultAdvisor..
能不能不配置代理bean的属性?即不指定哪些方法做了什么增强?
将userServiceProxy替换为一个DefaultAdvisorAutoProxyCreator类型的bean即可
<!-- 动态代理,自动生成代理对象-->
<bean id="userServiceProxy" class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />
基于aop标签
Spring从2.0开始,提供了
需要在pom.xml中添加如下依赖:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
简单示例
使用示例:
<!--被代理bean-->
<bean id="userService" class="com.bailiban.day4.aop.dynamic_proxy.noproxy.UserServiceImpl" />
<!-- 代理通知bean -->
<bean id="logAdvice" class="com.bailiban.day4.aop.aopxml.LogAdvice" />
<!-- 环绕切口-->
<bean id="aroundAdvice" class="com.bailiban.day4.aop.aopxml.AroundAdvice" />
<!-- aop相关配置-->
<aop:config>
<!-- 定义切面,该切面的增强方法在logAdvice中定义的-->
<aop:aspect ref="logAdvice">
<!-- 指定在哪些类的哪些方法代理
execution(* com.bailiban.day5.aop.noproxy.UserServiceImpl.*(..))
execution:指定方法签名;
*:表示任意字符,第一个* 表示方法的限定符(private,public),此处表示任意;
com.bailiban.day5.aop.noproxy.UserServiceImpl:指定类名;
第二个* :表示任意方法,即UserServiceImpl类下面的所有方法;
(..):指定参数,此处使用..表示任意参数。注意".."也可以用来表示任意子包。
-->
<aop:pointcut id="log" expression="execution(* com..noproxy.UserServiceImpl.*(..))"/>
<!-- 指定在代理方法运行结束之后,执行增强方法-->
<aop:after method="logger" pointcut-ref="log" />
</aop:aspect>
<!-- 定义切面,该切面的增强方法在aroundAdvice中定义的-->
<aop:aspect ref="aroundAdvice">
<aop:pointcut id="arround" expression="execution(* com..noproxy.UserServiceImpl.*(..))"/>
<aop:around method="around" pointcut-ref="arround" />
</aop:aspect>
</aop:config>
关键点:
<aop:config>
:表明这是一个aop配置;<aop:aspect ref="logAdvice">
配置切面,ref为该切面使用的通知(即增强代码)<aop:pointcut>
:配置切入点
重要:expression="execution(* com..noproxy.UserServiceImpl.*(..))"
指定在哪里进行切入,即哪些方法会被增强。上面表达式表示UserServiceImpl类的所有方法都会被增强。<aop:around>
:指定切入的时机,这里是环绕,即被代理的方法前后都会被增强。
其他的切入点:
<aop:before>
:方法运行之前;<aop:after>
:方法运行结束,但还没有返回值;<aop:after-returning>
:方法运行结束,且可以获得返回值;<aop:after-throwing>
:在异常中切入;
测试:
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
"com/bailiban/day4/aop/aopxml/aop.xml");
UserService userService = (UserService) context.getBean("userService");
userService.getUser();
userService.createUser();
userService.updateUser();
userService.deleteUser();
}
运行结果:
开始执行...
getUser
结束执行...
2019-12-23 21:58:14: method getUser invoked.
开始执行...
createUser
结束执行...
2019-12-23 21:58:14: method createUser invoked.
开始执行...
updateUser
结束执行...
2019-12-23 21:58:14: method updateUser invoked.
开始执行...
deleteUser
结束执行...
2019-12-23 21:58:14: method deleteUser invoked.
示例:事务处理
####### 手写事务
- 编写一个查询功能;
package com.bailiban.day5.aop.springaop.aop.transfer;
import java.sql.*;
public class JdbcUtil {
private static String jdbcUrl = "jdbc:mysql://192.168.1.132:3306/spring_demo?serverTimezone=Asia/Shanghai";
private static String username = "root";
private static String password = "root";
private static String driver = "com.mysql.jdbc.Driver";
public static void main(String[] args) {
// 1. Class.forName,加载用于mysql的驱动程序
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// Java7 支持try-with-resource语法,只要在try语句里面创建的资源,会自动关闭。
try (
// 2. 建立数据库连接
Connection conn = DriverManager.getConnection(jdbcUrl, username, password);
// 3. 创建statement
Statement statement = conn.createStatement();
// 4. 查询语句,并返回结果
ResultSet rs = statement.executeQuery("select id, name, money, role from account");
) {
// 5. 遍历查询结果
while (rs.next()) {
System.out.println(rs.getInt(1) + "\t" +
rs.getString(2) + "\t" +
rs.getDouble(3) + "\t" +
rs.getString(4));
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
- 编写非事务的转账功能
public void transfer(int fromId, int toId, double money) throws SQLException {
Account fromAccount = accountDao.findById(fromId);
Account toAccount = accountDao.findById(toId);
fromAccount.setMoney(fromAccount.getMoney() - money);
accountDao.update(fromAccount);
// 此处可以手动抛出一个异常,此时钱被转走,但接收者并不会收到。
// if (true)
// throw new RuntimeException();
toAccount.setMoney(toAccount.getMoney() + money);
accountDao.update(toAccount);
}
当没有事务管理时,程序功能可能出现异常。例如,上述最后一句accountDao.update(toAccount);
执行失败(即接收者没有收到钱),而此时accountDao.update(fromAccount);
以及执行成功了。就会出现钱减少,但没到账的问题。
- 编写支持事务的转账代码
public void transfer2(int fromId, int toId, double money) {
Account fromAccount = accountDao.findById(fromId);
Account toAccount = accountDao.findById(toId);
try {
System.out.println("begin");
JdbcUtil.connection.setAutoCommit(false);
fromAccount.setMoney(fromAccount.getMoney() - money);
accountDao.update(fromAccount);
// if (true)
// throw new RuntimeException();
toAccount.setMoney(toAccount.getMoney() + money);
accountDao.update(toAccount);
System.out.println("commit");
JdbcUtil.connection.commit();
} catch (Exception e) {
e.printStackTrace();
try {
System.out.println("rollback");
JdbcUtil.connection.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
}
关键点:
- 开启事务
JdbcUtil.conn.setAutoCommit(false);
- 提交事务
JdbcUtil.conn.commit();
- 事务失败时,回滚事务
JdbcUtil.conn.rollback();
JdbcUtil 定义见:https://github.com/Jun67/spring-demo/blob/master/src/main/java/com/bailiban/day4/aop/aopxml/transaction/JdbcUtil.java
####### 使用AOP写事务
- 添加四个拦截方法
- 开始事务
// 开始事务,用于 before advice
public void transaction_begin() throws SQLException {
System.out.println("begin");
connection.setAutoCommit(false);
}
- 提交事务
// 提交事务,用于 after-returning advice
public void transaction_commit() throws SQLException {
System.out.println("commit");
connection.commit();
}
- 回滚事务
// 回滚事务,用于 after-throwing advice
public void transaction_rollback() throws SQLException {
System.out.println("rollback");
connection.rollback();
}
- 回收资源
// 回收资源,此处并没有关闭连接,对应于 after advice
public void transaction_release() {
System.out.println("release");
}
- 将拦截器配置到transfer方法上
<!-- 事务处理相关方法-->
<bean id="jdbcUtil" class="com.bailiban.day4.aop.aopxml.transaction.JdbcUtil" />
<!-- 事务AOP配置-->
<aop:config>
<aop:aspect ref="jdbcUtil">
<!-- 拦截transfer方法-->
<aop:pointcut id="pc1" expression="execution(* com..transaction.*.transfer(..))"/>
<!-- transfer运行前,开启事务-->
<aop:before method="transaction_begin" pointcut-ref="pc1" />
<!-- transfer运行结束后,提交事务-->
<aop:after-returning method="transaction_commit" pointcut-ref="pc1" />
<!-- 异常回滚-->
<aop:after-throwing method="transaction_rollback" pointcut-ref="pc1" />
<!-- 即使出现异常,after方法也会执行,模拟连接资源回收-->
<aop:after method="transaction_release" pointcut-ref="pc1" />
</aop:aspect>
</aop:config>