本人小白一枚,欢迎大家一起讨论学习,如有错误,还望大家指教。
AOP概述
AOP的概念:
AOP,全称Apect Oriented Programming,译为面向切面编程,简单的说它可以帮我们把程序中重复的代码抽取出去,在需要执行的时候,使用动态代理技术,在不修改源码的基础上,对我们已有的方法进行增强。
AOP的作用及优势:
作用:在程序运行期间,不修改源码对已有的方法进行增强。
优势:减少重复代码,提高开发效率,维护方便。
AOP相关术语:
Joinpoint(连接点)
:所谓连接点是指那些被拦截到的点。在Spring中,这些点指的是方法,因为spring只支持方法类型的连接点。
Pointcut(切入点)
:所谓切入点是指我们要对哪些Joinpoint进行拦截的定义。
Advice(通知/增强)
:所谓通知是指拦截到Joinpoint之后要做的事情就是五种。通知的类型有五种:分别是前置通知,后置通知,异常通知,最终通知以及环绕通知。
Introduction(引介)
:引介是一种特殊的通知,是在不修改类代码的前提下,Introduction
可以在运行期间为类动态地添加一些方法或Field。
Target(代理对象)
:代理的目标对象。
Weaving(织入)
:是指把增强应用到目标对象来创建新的代理对象的过程,spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。
Proxy(代理)
:一个类被AOp植入增强后,就产生一个结果代理类。
Apect(切面)
:是切入点和通知(引介)的结合。
基于XML的AOP配置
<aop:config></aop:config>
:用于声明开始aop的配置,aop的相关配置都在这个标签之内。<aop:aspect></aop:aspect>
:用于配置切面信息,属性id
用于给切面提供一个唯一的表示,属性ref
用于引用配置好的通知类bean的id。<aop:pointcut></aop:pointcut>
:用于配置切入点表达式,就是指定对哪些类的那些方法进行增强。属性expression
用于定义切入点表达式,id
用于给切入点表达式提供一个唯一标识。<aop:xxx></aop:xxx>
:用于配置对应的通知类型,这个标签在<aop:aspect></aop:aspect>
标签中使用。<aop:before></aop:before>
:用于配置前置通知,在切入点方法之前执行。method
:用于指定通知类中的增强方法的名称。pointcut-ref
:用于指定切入点的表达式的引用。pointcut
:用于指定切入点表达式。
<aop:after-returning></aop:after-returning>
:用于配置后置通知,在切入点方法正常执行后再执行,它和异常通知只能有一个执行,该标签具有的属性同上。<aop:after-throwing></aop:after-throwing>
:用于配置异常通知,在切入点方法发生异常后执行,它和后置通知只能执行一个,该标签具有的属性同上。<aop:after></aop:after>
:用于配置最终通知,无论切入点执行时是否发生异常,它都会在后面执行,该标签具有的属性同上。<aop:around></aop:around>
:用于配置环绕通知,它是spring框架为我们提供的一种可以手动控制增强代码什么时候执行的方式,通常情况下,环绕通知都是独立使用的,该标签具有的属性同上。
execution
:对匹配到的方法进行增强,后接表达式,表达式语法:execution([修饰符] 返回值类型 包名.类名.方法名(参数))
。public void com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)
:对指定路径下的saveAccount方法且该方法有一个参数,类型为指定路径的Account。* com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)
:返回值可以使用*号,表示任意返回值。* *.*.*.*.AccountServiceImpl.saveAccount(com.itheima.domain.Account)
:包名可以使用*号,表示任意包,但是有几级包,需要写几个* 。* com..AccountServiceImpl.saveAccount(com.itheima.domain.Account)
:使用…来表示当前包,及其子包 。* com..*.saveAccount(com.itheima.domain.Account)
:类名可以使用*号,表示任意类 。* com..*.*( com.itheima.domain.Account)
:方法名可以使用*号,表示任意方法。* com..*.*(*)
:参数列表可以使用*,表示参数可以是任意数据类型,但是必须有参数 。* com..*.*(..)
:参数列表可以使用…表示有无参数均可,有参数可以是任意类型。* *..*.*(..)
:全通配方式,表示所有包下的所有方法,通常情况下我们只对业务层代码进行增强,所以切入点表达式都是指定业务层实现类。如按照上述路径,我们应该这么写execution(* com.itheima.service.impl.*.*(..))
。
使用演示:同样采用转账案例。
- 创建maven工程,并在其中添加所需的坐标。
<dependencies>
<!--spring坐标-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!--dbutils坐标-->
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.4</version>
</dependency>
<!--数据库驱动坐标-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
<!--连接池坐标-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.9</version>
</dependency>
<!--添加对aop相关的坐标-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
</dependencies>
- 创建entity包,并添加Account实体类,下图展示数据表的结构以及记录数。
public class Account implements Serializable {
private Integer id;
private String name;
private Float money;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Float getMoney() {
return money;
}
public void setMoney(Float money) {
this.money = money;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", money=" + money +
'}';
}
}
- 创建utils包并添加获取连接工具类以及事务管理类。
/**
* 获取连接工具类,用于从连接池中获取一个连接,并且实现连接与线程绑定。
*/
public class ConnectionUtils {
private ThreadLocal<Connection> tl;
private DataSource dataSource;
public void setTl(ThreadLocal<Connection> tl) {
this.tl = tl;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public Connection createConnection() {
Connection connection = null;
try {
// 先从ThreadLocal获取连接
connection = tl.get();
// 如果当前线程没有连接,则从数据源中获取一个连接,并将连接与线程绑定
if (connection == null) {
connection = dataSource.getConnection();
tl.set(connection);
}
return connection;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 将连接和线程解绑
*/
public void removeConnection() {
tl.remove();
}
}
/**
* 事务管理工具类,它包含了开始事务,提交事务,回滚事务和释放连接
*/
public class TransactionManager {
private ConnectionUtils utils;
public void setUtils(ConnectionUtils utils) {
this.utils = utils;
}
/**
* 关闭自动提交,开启手动事务
*/
public void beginTransaction() {
try {
utils.createConnection().setAutoCommit(false);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 提交事务
*/
public void commit() {
try {
utils.createConnection().commit();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 回滚事务
*/
public void rollback() {
try {
utils.createConnection().rollback();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 释放连接,这里使用了连接池技术,所以这里的释放就是将连接归还到连接池,同时别忘了将线程与连接进行解绑
*/
public void close() {
try {
utils.createConnection().close();
utils.removeConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
- 创建dao包并添加对应接口以及实现类。
public interface AccountDao {
/**
* 通过姓名查找账户信息
* @param name 账户姓名
* @return
*/
Account findByName(String name);
/**
* 修改账户信息
* @param account
*/
void updateAccount(Account account);
}
public class AccountDaoImpl implements AccountDao {
private QueryRunner runner;
private ConnectionUtils utils;
public void setRunner(QueryRunner runner) {
this.runner = runner;
}
public void setUtils(ConnectionUtils utils) {
this.utils = utils;
}
@Override
public Account findByName(String name) {
List<Account> accounts = null;
try {
accounts = runner.query( utils.createConnection(), "SELECT * FROM account WHERE name = ?", new BeanListHandler<Account>(Account.class), name);
if(accounts == null || accounts.size() == 0){
return null;
}
if(accounts.size() > 1){
throw new RuntimeException("结果集不唯一,数据有问题");
}
return accounts.get(0);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public void updateAccount(Account account) {
try {
runner.update( utils.createConnection(), "UPDATE account SET name = ?, money = ? WHERE id = ? ", account.getName(), account.getMoney(), account.getId());
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
- 创建service包并添加对应接口以及实现类。
public interface AccountService {
/**
* 更新用户
*/
void updateAccount(Account account);
/**
* @param sourceName 转出账户名称
* @param targetName 转入账户名称
* @param money 转账金额
*/
void transferAccount(String sourceName, String targetName, Float money);
}
public class AccountServiceImpl implements AccountService {
private AccountDao dao;
public AccountDao getDao() {
return dao;
}
public void setDao(AccountDao dao) {
this.dao = dao;
}
@Override
public void updateAccount(Account account) {
dao.updateAccount(account);
}
@Override
public void transferAccount(String sourceName, String targetName, Float money) {
Account sourceAccount = dao.findByName(sourceName);
Account targetAccount = dao.findByName(targetName);
sourceAccount.setMoney(sourceAccount.getMoney() - money);
targetAccount.setMoney(targetAccount.getMoney() + money);
dao.updateAccount(sourceAccount);
// 手动添加异常
int i = 10 / 0;
dao.updateAccount(targetAccount);
}
}
- 在resource目录下创建xml配置文件,并在其中添加配置信息,这里要注意一下,以上类的属性要加上set方法,因为这里我们要都是用的是set方式进行注入的。
<?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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置持久层-->
<bean id="accountDao" class="dao.impl.AccountDaoImpl">
<property name="utils" ref="connect"/>
<property name="runner" ref="runner"/>
</bean>
<!--配置事务层-->
<bean id="accountService" class="service.impl.AccountServiceImpl">
<property name="dao" ref="accountDao"/>
</bean>
<bean id="tl" class="java.lang.ThreadLocal"/>
<!--配置连接工具类-->
<bean id="connect" class="utils.ConnectionUtils">
<property name="dataSource" ref="dataSource"/>
<property name="tl" ref="tl"/>
</bean>
<!--配置事务管理类-->
<bean id="transactionManager" class="utils.TransactionManager">
<property name="utils" ref="connect"/>
</bean>
<!--配置数据库操作对象-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner">
<constructor-arg name="ds" ref="dataSource"/>
</bean>
<!--配置数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///frame?characterEncoding=utf-8"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
<!--声明事务-->
<aop:config>
<aop:pointcut id="pt1" expression="execution(* service.impl.*.*(..))"></aop:pointcut>
<aop:aspect id="txAdvice" ref="transactionManager">
<aop:before method="beginTransaction" pointcut="execution(* service.impl.*.*(..))"/>
<aop:after-returning method="commit" pointcut-ref="pt1"/>
<aop:after-throwing method="rollback" pointcut-ref="pt1"/>
<aop:after method="close" pointcut-ref="pt1"/>
<!--<aop:around method="transactionAround" pointcut-ref="pt1"/>-->
</aop:aspect>
</aop:config>
</beans>
- 创建controller包并添加测试方法,因为我在业务层的转账操作手动添加了异常,看看转账操作是否成功回滚。
public class Demo {
public static void main(String[] args) {
try {
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
AccountService service = context.getBean("accountService", AccountService.class);
System.out.println("开始转账~~~");
service.transferAccount("周星星", "刘灰灰", 500f);
System.out.println("转账成功~~~");
} catch (Exception e) {
System.out.println("转账失败~~~");
}
}
}
- 因为在业务层的转账操作去除异常并查看结果。
- 对于xml方式配置aop的总结,通过上面的测试我们发现,我们成功的将事务增强到业务层的方法中,但是我们只使用了前置通知,后置通知,异常通知以及最终通知,并没有使用环绕通知,所以在这里补充下环绕通知。
- 在事务管理类中添加一个方法,用以表示环绕通知,环绕通知就是我们可以自定义增强方法的执行顺序。spring为我们提供了一个接口让我们可以更便利的定义该方法。
- 修改xml配置文件,添加环绕通知的配置信息,同时将其余四个通知注释,再次测试,成功运行。
基于注解的AOP配置
@Aspect
:用于声明当前类为切面类,在类上使用。@Before
:用于声明该方法为前置通知,在方法上使用。value
:用于指定切入点表达式,还可以指定切入点表达式的引用。
@AfterReturning
:用于声明该方法为后置通知,在方法上使用,该注解的属性同上。@AfterThrowing
:用于声明该方法为异常通知,在方法上使用,该注解的属性同上。@After
:用于声明该方法为最终通知,在方法上使用,该注解的属性同上。@Around
:用于声明该方法为环绕通知,在方法上使用,该注解的属性同上。@Pointcut
:指定切入点表达式,在通知类的方法上使用。value
:指定表达式的内容。
使用演示:同样采用转账案例,这里使用纯注解方式。
-
创建maven工程,并在其中添加所需的坐标,直接复制上一个案例的pom文件即可。
-
创建entity包,并添加Account实体类,这里同上使用上一个案例的数据表,同时将记录恢复到演示前。
-
创建utils包并添加获取连接工具类以及事务管理类,因为这里使用了注解,我们就可以不用添加set方法。
/**
* 获取连接工具类,用于从连接池中获取一个连接,并且实现连接与线程绑定。
*/
@Component
public class ConnectionUtils {
@Autowired
private ThreadLocal<Connection> tl;
@Autowired
private DataSource dataSource;
public Connection createConnection() {
Connection connection = null;
try {
// 先从ThreadLocal获取连接
connection = tl.get();
// 如果当前线程没有连接,则从数据源中获取一个连接,并将连接与线程绑定
if (connection == null) {
connection = dataSource.getConnection();
tl.set(connection);
}
return connection;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 将连接和线程解绑
*/
public void removeConnection() {
tl.remove();
}
}
@Component
// 使用该注解表明这是一个切面
@Aspect
public class TransactionManager {
@Autowired
private ConnectionUtils utils;
@Pointcut("execution(* service.impl.*.*(..))")
private void pt1() {}
/**
* 关闭自动提交,开启手动事务
*/
@Before("pt1()")
public void beginTransaction() {
try {
utils.createConnection().setAutoCommit(false);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 提交事务
*/
@AfterReturning("execution(* service.impl.*.*(..)))")
public void commit() {
try {
utils.createConnection().commit();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 回滚事务
*/
@AfterThrowing("pt1()")
public void rollback() {
try {
utils.createConnection().rollback();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 释放连接,这里使用了连接池技术,所以这里的释放就是将连接归还到连接池,同时别忘了将线程与连接进行解绑
*/
@After("pt1()")
public void close() {
try {
utils.createConnection().close();
utils.removeConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
- 创建dao包并添加对应接口以及实现类。
public interface AccountDao {
/**
* 通过姓名查找账户信息
* @param name 账户姓名
* @return
*/
Account findByName(String name);
/**
* 修改账户信息
* @param account
*/
void updateAccount(Account account);
}
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {
@Autowired
private QueryRunner runner;
@Autowired
private ConnectionUtils utils;
@Override
public Account findByName(String name) {
List<Account> accounts = null;
try {
accounts = runner.query( utils.createConnection(), "SELECT * FROM account WHERE name = ?", new BeanListHandler<Account>(Account.class), name);
if(accounts == null || accounts.size() == 0){
return null;
}
if(accounts.size() > 1){
throw new RuntimeException("结果集不唯一,数据有问题");
}
return accounts.get(0);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public void updateAccount(Account account) {
try {
runner.update( utils.createConnection(), "UPDATE account SET name = ?, money = ? WHERE id = ? ", account.getName(), account.getMoney(), account.getId());
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
- 创建service包并添加对应接口以及实现类。
public interface AccountService {
/**
* 更新用户
*/
void updateAccount(Account account);
/**
* @param sourceName 转出账户名称
* @param targetName 转入账户名称
* @param money 转账金额
*/
void transferAccount(String sourceName, String targetName, Float money);
}
@Service("accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao dao;
@Override
public void updateAccount(Account account) {
dao.updateAccount(account);
}
@Override
public void transferAccount(String sourceName, String targetName, Float money) {
Account sourceAccount = dao.findByName(sourceName);
Account targetAccount = dao.findByName(targetName);
sourceAccount.setMoney(sourceAccount.getMoney() - money);
targetAccount.setMoney(targetAccount.getMoney() + money);
dao.updateAccount(sourceAccount);
// 手动添加异常
// int i = 10 / 0;
dao.updateAccount(targetAccount);
}
}
- 创建config包,并添加配置类以及数据库配置类,同时数据库配置类引用了db.properties文件。
@PropertySource("db.properties")
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean("dataSource")
public DataSource createDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
@Bean("runner")
public QueryRunner createQueryRunner(@Qualifier("dataSource")DataSource dataSource) {
return new QueryRunner(dataSource);
}
}
@Configuration
@ComponentScan({"dao", "service", "utils"})
// 该注解表示开启对AOP的支持
@EnableAspectJAutoProxy
@Import(JdbcConfig.class)
public class SpringConfig {
@Bean("tl")
public ThreadLocal createThreadLocal() {
return new ThreadLocal();
}
}
- 创建controller包并添加测试方法,
注意,此时的转账操作并没有异常
,这里可以对比下使用xml方式以及注解方式,获取ApplicationContext
的方式是不一样的,当我们执行测试时,在没有异常的情况下,我的猜想是顺利转账,但是结果却是报出了一个异常,根据异常信息显示,它提示我们不能进行commit,也就是事务提交操作,因为现在是自动提交事务即autocommit=true。原因就是当我们使用注解方式配置AOP时,spring有个bug,就是它会先执行最终通知,再回执行后置通知,也就是后置通知与最终通知的顺序是反的,想要解决这个问题,我们可以使用环绕通知来解决问题。
public class Demo {
public static void main(String[] args) {
try {
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
AccountService service = context.getBean("accountService", AccountService.class);
System.out.println("开始转账~~~");
service.transferAccount("周星星", "刘灰灰", 500f);
System.out.println("转账成功~~~");
} catch (Exception e) {
e.printStackTrace();
System.out.println("转账失败~~~");
}
}
}
- 在通知类添加环绕通知,已解决上述的问题,这里注意一下,因为使用环绕通知,所以将以上四个通知的注解全部注释。
@Component
// 使用该注解表明这是一个切面
@Aspect
public class TransactionManager {
@Autowired
private ConnectionUtils utils;
@Pointcut("execution(* service.impl.*.*(..))")
private void pt1() {}
/**
* 关闭自动提交,开启手动事务
*/
// @Before("pt1()")
public void beginTransaction() {
try {
utils.createConnection().setAutoCommit(false);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 提交事务
*/
// @AfterReturning("execution(* service.impl.*.*(..)))")
public void commit() {
try {
utils.createConnection().commit();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 回滚事务
*/
// @AfterThrowing("pt1()")
public void rollback() {
try {
utils.createConnection().rollback();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 释放连接,这里使用了连接池技术,所以这里的释放就是将连接归还到连接池,同时别忘了将线程与连接进行解绑
*/
// @After("pt1()")
public void close() {
try {
utils.createConnection().close();
utils.removeConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 使用环绕通知来解决后置通知与最终通知的顺序问题
*/
@Around("pt1()")
public Object transactionAround(ProceedingJoinPoint pjp) {
Object rtValue = null;
try {
// 获取方法执行所需的参数
Object[] args = pjp.getArgs();
// 开启事务
beginTransaction();
// 执行方法
rtValue = pjp.proceed(args);
// 提交事务
commit();
} catch (Throwable e) {
// 回滚事务
rollback();
e.printStackTrace();
} finally {
// 释放资源
close();
}
return rtValue;
}
}
总结:这里就不对纯注解方式的AOP测试进行详细的描述了,大家可以自行测试,并且上述代码我已经亲测没有问题。同时我还要在强调一个问题,就是咋们使用纯注解时,可以在主配置类上使用@EnableAspectJAutoProxy
注解表示开始AOP支持,如果我们使用注解加xml混合配置时,我们如果在配置中开始对AOP的支持,结果肯定很简单地,就是在配置文件中添加以下注解,用以开启AOP的支持,在<beans></beans>
标签中。
<!-- 开启 spring 对注解 AOP 的支持 -->
<aop:aspectj-autoproxy/>