4、AOP部分
4.1 AOP概述
在OOP的开发中,对于一些重复的操作可以抽离成模块,这可以减少代码量,但还是无法从根本上解决代码的冗余。在这种情况下我们可以把这些重复的操作抽离成切面,通过在运行时动态代理组合进原有的对象,这就是AOP,它是对OOP的补充。
AOP即面向切面编程,实际上就是对一些方法进行业务上面的按需增强,将一些与业务逻辑无关的业务方法(如:日志打印、权限校验、数据缓存等)抽离开来作为增强器,再利用动态代理进行增强,从这我们也可以体会到AOP也有实现解耦的作用,并且AOP 可以实现组件化、可插拔式的功能扩展。
AOP的设计原理是对原有业务逻辑的横切增强,底层是运行时动态代理机制。
不同于OOP以对象为关注的核心,AOP的核心是切面。
4.2 术语
①目标对象:指的是被代理对象,也就是那个需要被增强的对象;
②连接点:在Spring中,连接点指的是目标对象中的所有方法;
③切入点:指的是对目标对象进行增强的连接点,目标对象中的连接点可能很多,但需要增强的可能不是全部,所以切入点一定是连接点,但连接点不一定是切入点;
④通知:用来增强对象的那些代码(如:日志打印、记录等);
⑤代理对象:指的是目标对象和通知的组合
⑥切面:指的是切入点和通知的组合
4.3 通知类型
前置通知 Before:在目标方法(切入点方法)调用之前触发;
后置通知 After:在目标方法(切入点方法)调用之后触发;
返回通知 AfterReturing:在目标方法(切入点方法)成功返回之后触发;
异常通知 AfterThrowing:在目标方法(切入点方法)出现/抛出异常之后触发;
环绕通知 Around:它可以直接拿到目标对象,以及要执行的方法,所以可以在程序执行的任意位置进行切入。
代码语言:javascript复制try{
//前置通知
Object res = pjp.proceed();
//返回通知
}catch(e){
//异常通知
}finally{
//后置通知
}
4.4 切入点表达式
Spring中的AOP配置是根据切入点表达式去**找到特定的方法进行切入(增强)**,因此在实现AOP之前,我们需要了解切入点表达式的各种写法。
1)切入点表达式的语法:
execution(访问限定符 方法返回值类型 方法全类名(参数列表类型) [throws] 异常全类名 )
2)通配符
如果包名为 .. 则表示所有下级包(递归),如果参数为 .. 则表示不限制参数,如果包名/方法名为*表明全部包/方法,同时表达式中支持|| && 操作符
例如:
代码语言:javascript复制①execution(public int top.jtszt.impl.MyCalculator.*(int,int))
②execution(int top.jtszt..*.*(..))
①表示切入的是 top.jtszt.impl.MyCalculator
类下的所有 返回值为int型
且 带有两个int型参数
的 公有
的方法
②表示切入的是 top.jtszt所有下级子包
中的 所有类
下的所有返回值为int型
的公有
方法
4.5 AOP实现(基于xml)
背景:目标对象为top.jtszt.impl.MyCalculator,它是对top.jtszt.inter.Calculator接口的实现,其中有add、sub、div、multi这四个连接点,而切面类为top.jtszt.utils.LogUtils,其中有logStart、logReturn、logException、logEnd、logAround 这五个通知方法,现需要使用切面类对目标对象进行切入。
首先需要在maven导入AOP所需的依赖,包括spring-aop(被spring-context依赖)、aopalliance、 aspectjweaver 、cglib。接着在spring的配置文件中声明AOP配置,这里需要导入aop名称空间。
①切面类注入IoC:为切面类配置bean;
②配置切入点表达式:接着配置aop使用的是<aop:config>
标签,为了达到切入点表达式复用的效果,我们可以先使用 <aop:pointcut>
标签声明切入点,它的expression属性
即是切入点表达式,在下面我们只需要根据其id就可以复用这个表达式了;(注意:被切入的类必须注入IoC容器)
③定义切面类:使用<aop:aspect>
标签进行定义,ref属性
指向的是切面类bean,接着在标签体内定义各种通知方法;
④定义通知方法:有五个标签可以定义通知方法,在标签体内 method
属性为通知方法名, pointcut-ref属性
指向上面定义的切入点表达式。<aop:before>
代表前置通知;<aop:after-returning>
代表返回通知,可以使用returning属性定义接收return值的变量名,在切入方法中作为参数传入;<aop:after-throwing>
代表异常通知,可以使用throwing属性定义接收异常信息的变量名,在切入方法中作为参数传入;<aop:after>
代表后置通知;<aop:around>
代表环绕通知。
<beans>
<!-- 首先需要为切面类配置bean -->
<bean id="logUtils2" class="top.jtszt.utils.LogUtils"/>
<!-- 在配置文件中配置AOP -->
<aop:config>
<!-- 定义切入点表达式 -->
<aop:pointcut id="myPoint" expression="execution(public * top.jtszt.impl.MyCalculator.*(int,int))"/>
<!-- 定义一个切面类 -->
<aop:aspect ref="logUtils2">
<!--定义前置通知方法-->
<aop:before method="logStart" pointcut-ref="myPoint"/>
<!--定义返回通知方法-->
<aop:after-returning method="logReturn" pointcut-ref="myPoint" returning="result"/>
<!--定义异常通知方法-->
<aop:after-throwing method="logException" pointcut-ref="myPoint" throwing="exception"/>
<!--定义后置通知方法-->
<aop:after method="logEnd" pointcut-ref="myPoint"/>
<!--定义环绕通知方法-->
<aop:around method="logAround" pointcut-ref="myPoint" />
</aop:aspect>
</aop:config>
</beans>
4.6 AOP实现(基于注解)
1)切面类注入IoC:为切面类加上@Component与@Aspect注解
2)配置切入点表达式:在切面类中定义一个空方法,使用@Pointcut 注解声明切入点表达式,以便在下面复用这个表达式;
代码语言:javascript复制@Pointcut("execution(public int top.jtszt.impl.MyCalculator.*(int,int))")
public void pointcutExpression(){}
3)定义通知方法:
**① @Before()**:表明是在方法开始前切入;
**② @AfterReturning()**:表明是在方法正常返回后切入,后可声明接收返回值的参数名;
③ @AfterThrowing() :表明是在方法抛出异常后切入,后可声明接收异常的参数名;
④ @After() :表明是在方法最终结束时切入(如try..catch中的finally);
⑤ @Around() :表明这是一个环绕通知方法,环绕方法会先于其他四个通知方法执行,这个方法的返回值代表的就是调用实际方法的返回值。
代码语言:javascript复制@Before("pointcutExpression()")
public static void logStart(){}
@AfterReturning(value="pointcutExpression()",returning = "result")
public static void logReturn(Object result){}
@AfterThrowing(value="pointcutExpression()",throwing = "exception")
public static void logException(Exception exception){}
@After("pointcutExpression()")
public static void logEnd(){}
4)开启注解AOP
如果使用xml 注解,可以在xml中配置<aop:aspectj-autoproxy/>
开启注解aop。
如果使用纯注解,可以在配置类加上@EnableAspectJAutoProxy
注解开启注解aop 。
4.7 通知方法参数
像在使用原生的动态代理一样,如果需要在通知方法中获取切入方法的参数与方法名等信息,需要传入JoinPoint类型的参数。其中有几个比较常用的方法:
•Object JoinPoint.getTarget():获取未代理的目标对象•Object JoinPoint.getThis():获取代理对象•Object[] JoinPoint.getArgs():获取切入方法的参数列表•Signature JoinPoint.getSignature():获取方法签名•String Signature.getName():获取方法名•Method (MethodSignature)Signature.getMethod():获取方法信息
需要注意的是,由于环绕通知方法的返回值代表的就是调用实际方法的返回值,因此其中需要传入一个ProceedingJoinPoint
类型的参数,通过这个对象调用proceed()
方法可以得到实际方法的返回值,这个语句也相当于动态代理中调用invoke()
方法。
4.8 多切面执行顺序
如果有多个切面类对同一个方法进行切入,遵循从外到内的规则(按切面类名的 unicode 编码的十六进制顺序执行)。
如:外层切面类A为AspectOne,内层切面类B为AspectTwo。
执行顺序为:A前置通知方法→B前置通知方法→实际方法→B返回/异常通知方法→B后置通知方法→A返回/异常通知方法→A后置通知方法
如果想改变切面的执行顺序,可以通过@Order注解设置切面类优先级,传入一个int型参数,数值越小优先值越高,默认为最低优先级。
此外,同切面中的相同类型通知方法的执行顺序也是按照unicode编码顺序来。
4.9 用AOP做事务控制
背景信息:书店进行图书销售活动,并且会员在系统中存有余额信息,在用户购买图书之后系统需要减图书库存同时减用户余额,这是一个整体(一个事务)。现在需要用AOP做事务控制,保证两个操作的一致性。
流程:让Spring管理数据库连接池以及jdbcTemplate,DAO利用自动装配的jdbcTemplate进行数据库操作,Service做具体的结账方法;之后让Spring利用AOP对这个结账方法做事务控制。
1)环境准备
①添加maven依赖,包括mysql-connector-java、spring-tx、c3p0、spring-jdbc以及ioc、aop相关的依赖;
②准备数据库表
用户信息表:
代码语言:javascript复制CREATE TABLE t_account(
username VARCHAR(50) PRIMARY KEY,
balance INT
)
图书信息表:
代码语言:javascript复制CREATE TABLE t_book(
isbn VARCHAR(50) PRIMARY KEY,
book_name VARCHAR(50),
price INT
)
图书库存表:
代码语言:javascript复制CREATE TABLE t_book_stock(
isbn VARCHAR(50),
stock INT,
CONSTRAINT fk_isbn FOREIGN KEY(isbn) REFERENCES t_book(isbn)
)
操作数据库:
代码语言:javascript复制@Repository
public class BookDAO {
@Autowired
JdbcTemplate jdbcTemplate;
//减少余额的方法
public void updateBalance(String userName, int price){
String sql = "UPDATE t_account SET balance=balance-? WHERE username=?";
jdbcTemplate.update(sql,price,userName);
}
// 获取图书价格的方法
public int getPrice(String isbn){
String sql = "SELECT price FROM t_book WHERE isbn=?";
return jdbcTemplate.queryForObject(sql, Integer.class, isbn);
}
// 减库存的方法
public void updateStock(String isbn){
String sql = "UPDATE t_book_stock SET stock=stock-1 WHERE isbn=?";
jdbcTemplate.update(sql,isbn);
}
}
服务方法(为方便不写接口):
代码语言:javascript复制@Service
public class BookService {
@Autowired
private BookDAO bookDAO;
public void checkout(String username,String isbn){
//减库存
bookDAO.updateStock(isbn);
//减余额
bookDAO.updateBalance(username, bookDAO.getPrice(isbn));
}
}
xml中还有包扫描等操作这里就不贴了。
2)配置声明式事务(基于xml)
上面的Service方法是没有做事务管理的,一旦减库存方法执行完毕之后出现异常,那么该库存将被成功减去1,但是用户余额却没扣除,这显然是不行的。接着我们要对它进行事务的管理,首先是基于xml的配置,它依赖于tx和aop名称空间。
首先需要配置数据源,并且由于上文使用了jdbcTemplate自动装配,这里顺便配置它。
代码语言:javascript复制<beans>
<!-- 配置写在db.properties中 -->
<context:property-placeholder location="db.properties"/>
<!-- c3p0连接池 -->
<bean id="ds" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="${jdbc.user}"/>
<property name="password" value="${jdbc.password}"/>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}"/>
<property name="driverClass" value="${jdbc.driverClass}"/>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"/>
<property name="minPoolSize" value="${jdbc.minPoolSize}"/>
</bean>
<!-- 配置jdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="ds"/>
</bean>
</beans>
接着配置Spring提供的事务管理器,当使用JDBC/MyBatis进行持久化时可以使用DataSourceTransactionManager做事务管理器。
代码语言:javascript复制<bean id="tm" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="ds"/>
</bean>
接着需要告诉Spring哪些方法是事务方法,使用的是tx名称空间下的advice
标签,其中transaction-manager
指向事务管理器,该标签下的attributes
只有一个标签method
,name
属性用于匹配事务方法(可以使用通配符),此外还有其他的一些属性:
•timeout 设置超时自动终止事务并回滚; •read-only 设置事务为只读; •no-rollback-for 指定哪些异常不回滚,传入异常全类名,默认为空; •rollback-for 当方法触发异常时回滚,传入异常全类名;默认是捕捉所有运行时异常和错误; •isolation 修改事务的隔离级别; •propagation 指定事务的传播行为;
这些属性也可以在@Transactional 注解中配置。
代码语言:javascript复制<tx:advice id="myAdvice" transaction-manager="tm">
<!-- 指明哪些方法是事务方法-->
<tx:attributes>
<tx:method name="*"/>
<tx:method name="checkout" timeout="-1" read-only="false"/>
<tx:method name="get*" read-only="true"/>
</tx:attributes>
</tx:advice>
上面只是声明事务方法,但实际上还需要设置切入点才能进行事务管理,只有成功切入了才有后面的事务管理。也就是说事务方法一定是切入点,但切入点不一定是事务方法。
代码语言:javascript复制<aop:config>
<aop:pointcut id="txPoint" expression="execution(* top.jtszt.*.*.*(..))"/>
<!-- advice-ref:指向事务管理器的配置 -->
<aop:advisor advice-ref="myAdvice" pointcut-ref="txPoint"/>
</aop:config>
3)配置声明式事务(基于注解)
首先需要在配置类加上@EnableTransactionManagement
表示开启事务管理器,也可以在xml文件中开启基于注解的声明式事务。
@Configuration
@EnableTransactionManagement
@ComponentScan("top.jtszt")
public class ConfClass {}
代码语言:javascript复制<tx:annotation-driven transaction-manager="tm"/>
之后在配置类中配置上数据源与事务管理器
代码语言:javascript复制@Configuration
@EnableTransactionManagement
@ComponentScan("top.jtszt")
public class ConfClass {
//读配置文件
@Bean
public Properties properties() throws IOException {
Properties properties = new Properties();
properties.load(new FileReader("db.properties"));
return properties;
}
//配置数据源
@Bean
public ComboPooledDataSource dataSource(Properties properties) throws PropertyVetoException {
ComboPooledDataSource ds = new ComboPooledDataSource();
ds.setUser(properties.getProperty("jdbc.user"));
ds.setPassword(properties.getProperty("jdbc.password"));
ds.setJdbcUrl(properties.getProperty("jdbc.jdbcUrl"));
ds.setDriverClass(properties.getProperty("jdbc.driverClass"));
return ds;
}
//配置jdbcTemplate
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
//配置事务管理器
@Bean
public DataSourceTransactionManager dataSourceTransactionManager(DataSource dataSource){
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dataSource);
return tm;
}
}
接着还需要告诉Spring哪些方法是事务方法,使用的是@Transactional
,并且最好设置rollbackFor属性,这个注解还有其他的属性可以设置,与<tx:method>
相似。
@Transactional(rollbackFor = {Exception.class})
public void checkout(String username,String isbn){...}
此外@Transactional
还可以设置在类上,表示全部方法都是事务方法。
4)事务的传播行为
上面讲到了事务方法设置中有一个属性可以设置事务的传播行为,那么事务的传播行为是啥?
事务传播行为指的是一个事务方法被另一个事务方法调用时运行的方式。Spring中定义了其中传播行为,分别是:
•REQUIRED:如果当前有事务则在其中运行,否则新开一个事务,在自己的事务里运行 (事务的属性都继承于大事务);•REQUIRES_NEW:当前方法必须开启新事务,并在自己的事务里运行,如果有事务正在运行则挂起;•SUPPORTS:如果有事务在运行则方法在这个事务中运行,否则可以不运行在事务中;•NOT_SUPPORTED:当前方法不应运行在事务中,如果有运行的事务则将其挂起;•MANDATORY:当前方法必须运行在事务内部,否则抛出异常;•NEVER:当前方法不应运行在事务内部,否则抛出异常;•NESTED:如果有事务在运行则当前方法应该在这个事务的嵌套事务中运行,否则启动一个新事务,并在自己的事务中运行。
5)事务失效
一般情况下,事务失效会有如下场景:
•在SSM开发中Spring和SpringMVC是分管两个容器,这时如果SpringMVC扫描了@Service那么对于@controller注入的则是没有事务的方法,这会导致事务失效。因此声明式事务的配置必须由Spring容器加载。•如果@Transactional注解标注在接口上,但实现类使用 Cglib 代理,则事务会失效。你标注的是接口,但是Cglib代理时直接拿到实现类去构建代理对象,也就绕过了接口的事务管理。•事务默认捕捉RuntimeException,如果抛出Exception,默认不捕捉,事务失效,所以一般情况下都是显式声明捕捉Exception。•在Service 方法中自行 try-catch 异常处理,那么呈现给事务拦截器的就是没有异常的情况,自然也会导致事务失效。•同一个类中,一个方法调用了自身另一个带有事务控制的方法,直接调用时也会导致事务失效。
参考资料:
•Spring Framework 5.1.3.RELEASE文档[1]•从 0 开始深入学习 Spring-掘金小册[2]•JavaGuide-Spring[3]•Spring中单例Bean的线程安全问题-CSDN[4]•Spring Bean的生命周期-博客园[5]•Spring IOC 容器源码分析_Javadoop[6]•Spring5 系统架构-CSDN[7]•雷丰阳Spring、Spring MVC、MyBatis课程-bilibili[8]
相关链接
[1]
Spring Framework 5.1.3.RELEASE文档: https://docs.spring.io/spring-framework/docs/5.1.3.RELEASE/spring-framework-reference/
[2]
从 0 开始深入学习 Spring-掘金小册: https://juejin.cn/book/6857911863016390663/section
[3]
JavaGuide-Spring: https://snailclimb.gitee.io/javaguide-interview/#/./docs/e-1spring
[4]
Spring中单例Bean的线程安全问题-CSDN: https://blog.csdn.net/vipshop_fin_dev/article/details/109017732
[5]
Spring Bean的生命周期-博客园: https://www.cnblogs.com/zrtqsk/p/3735273.html
[6]
Spring IOC 容器源码分析_Javadoop: https://javadoop.com/post/spring-ioc
[7]
Spring5 系统架构-CSDN: https://blog.csdn.net/lj1314ailj/article/details/80118372
[8]
雷丰阳Spring、Spring MVC、MyBatis课程-bilibili: https://www.bilibili.com/video/BV1d4411g7tv