日志编号 | 说明 |
---|---|
C-2020-10-12 | 第一次创建 |
AOP:Aspect Oriented Programming 面向切面编程
OOP:Object Oriented Programming 面向对象编程
面向切面编程:基于OOP基础之上新的编程思想,OOP面向的主要对象是类,而AOP面向的主要对象是切面,在处理日志、安全管理、事务管理等方面有非常重要的作用。AOP是Spring中重要的核心点,虽然IOC容器没有依赖AOP,但是AOP提供了非常强大的功能,用来对IOC做补充。通俗点说的话就是在程序运行期间,将某段代码动态切入到指定方法的指定位置进行运行的这种编程方式。
@Aspect
注解(AspectJ 注解方式)来实现。IsModified
接口, 以便简化缓存机制(在AspectJ社区,引入也被称为内部类型声明(inter))。出于学习的目的,同样的例子,我们做两个版本,一个是用配置文件,另一个是通过注解。
首先,导入需要的POM信息。根据之前说到的Spring结构图可以看出来在使用AOP、aspects的时候,需要引入这两个相应的模块。
先给出POM信息
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.phlgroupId>
<artifactId>spring_csdnartifactId>
<version>1.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.13version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.2.9.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aopartifactId>
<version>5.2.9.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aspectsartifactId>
<version>5.2.9.RELEASEversion>
dependency>
dependencies>
project>
准备好POM之后,我这次选用了网上常见的一个例子进行编写,编写一个有两个入参,一个出参,都是整数的计算器,AOP在其中的作用是记录日志。
先给出计算器类:
package com.phl.spring.csdn.bean;
public class MyCalculator {
public Integer add(Integer i,Integer j) throws Exception{
return i + j;
}
public Integer sub(Integer i,Integer j) throws Exception{
return i - j;
}
public Integer mul(Integer i,Integer j) throws Exception{
return i * j;
}
public Integer div(Integer i,Integer j) throws Exception{
return i / j;
}
}
之后给出日志工具类:
package com.phl.spring.csdn.util;
import org.aspectj.lang.JoinPoint;
import java.util.Arrays;
public class LogUtil {
private void start(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println(methodName + "方法即将执行,入参是:" + Arrays.asList(args));
}
public static void stop(JoinPoint joinPoint,Integer data) {
String methodName = joinPoint.getSignature().getName();
System.out.println(methodName + "方法执行结束,执行结果是:" + data.toString());
}
public static void exception(JoinPoint joinPoint,Throwable th) {
String methodName = joinPoint.getSignature().getName();
System.out.println(methodName + "方法发生异常,异常信息是:" + th.getMessage());
th.printStackTrace();
}
public static void end(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println(methodName + "方法执行完毕");
}
}
接着给出对应的spring配置文件
<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
http://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="myCalculator" class="com.phl.spring.csdn.bean.MyCalculator">bean>
<bean id="logUtil" class="com.phl.spring.csdn.util.LogUtil">bean>
<aop:config>
<aop:pointcut id="baseCut" expression="execution(* com.phl.spring.csdn.bean.MyCalculator.*(Integer,Integer))"/>
<aop:aspect ref="logUtil">
<aop:before method="start" pointcut-ref="baseCut">aop:before>
<aop:after-returning method="stop" pointcut-ref="baseCut" returning="data">aop:after-returning>
<aop:after-throwing method="exception" pointcut-ref="baseCut" throwing="th">aop:after-throwing>
<aop:after method="end" pointcut-ref="baseCut">aop:after>
aop:aspect>
aop:config>
beans>
最后,是测试类。
package com.phl.spring;
import com.phl.spring.csdn.bean.MyCalculator;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestXmlMyCalculator {
@Test
public void test01(){
ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("application.xml");
MyCalculator myCalculator = (MyCalculator) classPathXmlApplicationContext.getBean("myCalculator");
try {
myCalculator.div(4,1);
} catch (Exception e){
}
}
}
这个讲解内容是匹配上面的例子进行的。有这么如下几个重点内容需要留意。
注意,如果大家觉得通过AOP的命名空间去查看里面的各个属性具体有什么太麻烦(确实也麻烦),那我们可以通过查看注解的代码达到同样的目的。
下面就给出基于注解的写法。
先给出MyCalculator类
package com.phl.spring.csdn.bean;
import org.springframework.stereotype.Component;
@Component
public class MyCalculator {
public Integer add(Integer i,Integer j) throws Exception{
return i + j;
}
public Integer sub(Integer i,Integer j) throws Exception{
return i - j;
}
public Integer mul(Integer i,Integer j) throws Exception{
return i * j;
}
public Integer div(Integer i,Integer j) throws Exception{
return i / j;
}
}
然后是LogUtil
package com.phl.spring.csdn.util;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
@Aspect
public class LogUtil {
@Pointcut("execution(* com.phl.spring.csdn.bean.MyCalculator.*(Integer,Integer))")
public void point() {}
@Before("point()")
private void start(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println(methodName + "方法即将执行,入参是:" + Arrays.asList(args));
}
@AfterReturning(returning = "data",value = "point()")
public static void stop(JoinPoint joinPoint, Integer data) {
String methodName = joinPoint.getSignature().getName();
System.out.println(methodName + "方法执行结束,执行结果是:" + data.toString());
}
@AfterThrowing(throwing = "th",value = "point()")
public static void exception(JoinPoint joinPoint, Throwable th) {
String methodName = joinPoint.getSignature().getName();
System.out.println(methodName + "方法发生异常,异常信息是:" + th.getMessage());
th.printStackTrace();
}
@After("point()")
public static void end(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println(methodName + "方法执行完毕");
}
}
最后是application.xml配置文件:
<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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.phl.spring.csdn">context:component-scan>
<aop:aspectj-autoproxy >aop:aspectj-autoproxy>
beans>
测试类还是之前的测试类,没有什么特别的。
在使用注解的时候,首先有一个前提,就是要熟悉注解的写法和用法。如果对这里有疑问的,可以点击这个链接去查看相应内容Java基础知识-07-C-2020-08-11.
所有涉及到数据库的应用中,都离不开事务管理。Spring AOP 提供了我们一种更简单的解决方案,声明式事务管理。
先提一句,与声明式事务管理对应的是编码式事务管理。
要使用声明式事务管理,首先需要引入对应的pom信息。需要除了上面使用AOP的那些依赖之外,还需要将数据库连接,事务,jdbc工具引入到项目中。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.phlgroupId>
<artifactId>spring_csdnartifactId>
<version>1.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.13version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.2.9.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aopartifactId>
<version>5.2.9.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aspectsartifactId>
<version>5.2.9.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-txartifactId>
<version>5.2.9.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-jdbcartifactId>
<version>5.2.9.RELEASEversion>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.21version>
dependency>
dependencies>
project>
因为这篇是讲解AOP,因此尽量弱化了其他工具主流工具的技术需求。在这个例子里,没有用各类的数据库连接池,没有引入mybatis,都在用Spring本身的内容。
<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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="classpath:db.properties">context:property-placeholder>
<context:component-scan base-package="com.phl.spring.csdn">context:component-scan>
<aop:aspectj-autoproxy >aop:aspectj-autoproxy>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${url}">property>
<property name="driverClassName" value="${driver}">property>
<property name="username" value="${username}">property>
<property name="password" value="${password}">property>
bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg name="dataSource" ref="dataSource">constructor-arg>
bean>
beans>
暂且科普一下,Spring JdbcTemplate,是Spring针对JDBC做的一个封装工具,现实项目很少会直接用Spring JdbcTemplate。大家对他就知道一下就行,这个就是个便于执行SQL的工具。
再浅谈一下事务,所谓事务,就是一系列操作数据库的动作,统一成一个单元。这个操作单元要么都成功,要么一旦有一个或者多个失败,则需要把数据回滚到这个操作单元执行之前的样子。
现在举一个例子,就是卖书。书有价格,库存信息,顾客有自己的金额信息。下面给出具体代码。
首先是给出事务管理的配置信息,在application.xml中添加如下配置。
<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"
xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<context:property-placeholder location="classpath:db.properties">context:property-placeholder>
<context:component-scan base-package="com.phl.spring.csdn">context:component-scan>
<aop:aspectj-autoproxy >aop:aspectj-autoproxy>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${url}">property>
<property name="driverClassName" value="${driver}">property>
<property name="username" value="${username}">property>
<property name="password" value="${password}">property>
bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg name="dataSource" ref="dataSource">constructor-arg>
bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource">property>
bean>
<tx:annotation-driven transaction-manager="transactionManager">tx:annotation-driven>
beans>
先对上面的配置文件进行简单说明,在这个配置文件中,已经引入了三个命名空间,分别是context,aop,tx,context是开启IOC容器注解扫描,aop的配置是开启基于注解的AOP,tx的配置是开启基于注解的事务管理控制模式。顺带提一句,这仅仅是因为两个例子写在了一起,所以在POM和application里感觉引入了很多。大家在自己写的时候,pom里的内容可以一个一个引进去,这样就更能看出来哪个dependency对应了哪些jar包。
下面给出相应的代码,首先是三个bean
package com.phl.trans.bean;
import org.springframework.stereotype.Component;
@Component
public class Account {
private String userName;
private Integer userId;
private Double balance;
@Override
public String toString() {
return "Account{" +
"userName='" + userName + '\'' +
", userId=" + userId +
", balance=" + balance +
'}';
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
public Account() {
}
public Account(String userName, Integer userId, Double balance) {
this.userName = userName;
this.userId = userId;
this.balance = balance;
}
}
package com.phl.trans.bean;
import org.springframework.stereotype.Component;
@Component
public class Book {
private String bookName;
private Double price;
private Integer bookId;
@Override
public String toString() {
return "Book{" +
"bookName='" + bookName + '\'' +
", price=" + price +
", bookId=" + bookId +
'}';
}
public String getBookName() {
return bookName;
}
public void setBookName(String bookName) {
this.bookName = bookName;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public Integer getBookId() {
return bookId;
}
public void setBookId(Integer bookId) {
this.bookId = bookId;
}
public Book(String bookName, Double price, Integer bookId) {
this.bookName = bookName;
this.price = price;
this.bookId = bookId;
}
public Book() {
}
}
package com.phl.trans.bean;
import org.springframework.stereotype.Component;
@Component
public class Stock {
private Integer bookStock;
private Integer bookId;
@Override
public String toString() {
return "Stock{" +
"bookStock=" + bookStock +
", bookId=" + bookId +
'}';
}
public Integer getBookStock() {
return bookStock;
}
public void setBookStock(Integer bookStock) {
this.bookStock = bookStock;
}
public Integer getBookId() {
return bookId;
}
public void setBookId(Integer bookId) {
this.bookId = bookId;
}
public Stock() {
}
public Stock(Integer bookStock, Integer bookId) {
this.bookStock = bookStock;
this.bookId = bookId;
}
}
然后给出三个dao
package com.phl.trans.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class AccountDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public int decreaseBalanceByUserId(Double price,Integer userId){
String sql = "update user set balance = balance-? where user_id = ?";
int update = jdbcTemplate.update(sql, price, userId);
return update;
}
}
package com.phl.trans.dao;
import com.phl.trans.bean.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class BookDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public Book searchBookInfoByBookId(Integer id){
String sql = "select * from book where book_id = ?";
Book book = this.jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Book.class), id);
return book;
}
}
package com.phl.trans.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class StockDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public int decreaseStockByBookId(Integer bookId){
String sql = "update stock set book_stock = book_stock-1 where book_id = ?";
int update = jdbcTemplate.update(sql, bookId);
return update;
}
}
然后是负责买书的service,这个service里只写了一个方法,并且在这个方法上加上了@Transactional注解
package com.phl.trans.service;
import com.phl.trans.bean.Book;
import com.phl.trans.dao.AccountDao;
import com.phl.trans.dao.BookDao;
import com.phl.trans.dao.StockDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BuyBookService {
@Autowired
private AccountDao accountDao;
@Autowired
private BookDao bookDao;
@Autowired
private StockDao stockDao;
private Integer bookId = 1;
private Integer userId = 1;
@Transactional
public void BuyBook(){
Book book = bookDao.searchBookInfoByBookId(bookId);
accountDao.decreaseBalanceByUserId(book.getPrice(), userId);
stockDao.decreaseStockByBookId(bookId);
}
}
最后是对应的测试类
package com.phl.noioc.test;
import com.phl.trans.service.BuyBookService;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.sql.SQLException;
public class Test {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
@org.junit.Test
public void test01() throws SQLException {
BuyBookService service = (BuyBookService) context.getBean("buyBookService");
service.BuyBook();
}
}
在上面的例子中,我故意把库存表(book_stock)的表名在stockDao中写成了(book),这样会在运行的时候出错。
然后,如果service中的方法加上了Transactional注解,那么第一个成功的语句(更改账户余额)也会被回滚。如果没有加上注解,则第一个语句还是成功。
在@Transactional注解中有如下属性可以配置:
上面给出了各个属性的解释,下面逐个进行讲解。
最后就是propagation,事务的传播特性,也叫事务的传播行为。
注意,对于Transactional 声明式事务管理,如果方法内部catch,是不会进行回滚的。我的例子中,同样没有进行try catch。但是这种情况在实际使用中却不行,因为会引起程序终止。
要想在catch之后还回滚,需要在catch里面throw 一个新的异常,并且rollBackFor里面添加好对应的异常信息,通过rollBackFor或者noRollBackFor来控制。
再注意,假设在类A中有多个方法,如方法A1,方法A2,方法A3.给这3个方法上,都加上事务管理的注解。在类B中有调用方法,方法B1,在方法B1中去调用A类中的单个或者多个方法,此时,才会应用到传播特性。即,只有这种不同类之间的调用,才会出现事务的传播特性。
如果是在A类中,现在出现了方法A4,在A4中去调用A1,A2,A3。此时,不论1,2,3上面的传播特性定义的是什么,都将认为是最普通的当前类内的调用,这种调用关系下,就是统一用同一个事务,不存在传播特性。
这个是因为在当前类相互调用的时候,这中调用关系,最终不会通过AOP去进行织入。因此不牵扯传播特性。
传播特性名称 | 意义 | 详细运行结果说明 |
---|---|---|
Propagation.MANDATORY | 当前方法必须在一个事务中进行运行,如果当前没有事务正在发生,将抛出一个异常。 | |
Propagation.NEVER | 当前方法不应该在一个事务中运行。如果当前有事务正在进行,则会抛出一个异常。 | |
Propagation.NOT_SUPPORTED | 当前方法不应该在一个事务中运行。如果当前有事务正在进行,则会在事务执行期间挂起等事务结束后,再执行 | |
Propagation.SUPPORTS | 当前方法不必要挂载在事务中运行。但如果已经有一个运行的事务,则这个方法也可以在当前事务里进行运行。 | |
Propagation.REQUIRED | 当前方法必须在一个事务中运行。如果当前有正在进行的事务,这个方法就会在当前的事务中进行运行。如果当前没有事务,就会开始一个新的事务,确保这个方法的执行。 | 如果类A中的被调用方法上传播特性都是REQUIRED,那整体过程会在同一个事务中。一旦发生异常,整个都会回滚 |
Propagation.REQUIRES_NEW | 当前方法必须在事务中进行运行。并且每次调用时,一个新的事务都会被启动,这个方法会在新的事务中进行运行。如果当前已经有正在运行的事务,那么正在运行的事务,会在这个新事务运行期间被挂起。等新的事务运行完成之后,再继续执行。 | 对于REQUIRES_NEW,不论整个事务处理过程中是否出现异常,只要自己的事务正常,就会提交。同样,只要自己的事务异常,就会回滚。 |
Propagation.NESTED | NESTED,是一个“嵌套”的事务。他是已经存在事务的一个真正的子事务。当嵌套事务开始执行时,他会得到一个savepoint。如果这个嵌套事务失败,我们将回滚到这个savepoint。 | 与REQUIRES_NEW最大的区别就在于。虽然自己的事务执行成功,但是外层主事务要是出现异常,那么他也会回滚。 |
下面用一个场景去详细的描述上面说到的Propagation.REQUIRED,Propagation.REQUIRES_NEW,Propagation.NESTED这三个详细分析的传播特性。
假设在方法A中调用方法B
异常状态\传播特性 | Propagation.REQUIRES_NEW(两个独立事务) | Propagation.NESTED(B嵌套在A中) | Propagation.REQUIRED(同一个事务) |
---|---|---|---|
方法A异常,方法B正常 | 方法A回滚,方法B提交 | A和B一同回滚 | A和B一同回滚 |
方法A正常,方法B异常 | 如果A中捕获了B的异常,并没有继续向上抛出,则B先回滚,A在正常提交。如果A捕获了B的异常,并且继续向上抛出,则B先回滚,A再回滚 | B先回滚,A再正常提交 | A,B一同回滚 |
方法A异常,方法B异常 | B先回滚,A再回滚 | A和B一同回滚 | A和B一同回滚 |
方法A正常,方法B正常 | B先提交,A再提交 | A和B一同提交 | A和B一同提交 |