本人小白一枚,欢迎大家一起讨论学习,如有错误,还望大家指教。
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;}@Overridepublic 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;}@Overridepublic 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);}}@Overridepublic 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;}@Overridepublic void updateAccount(Account account) {dao.updateAccount(account);}@Overridepublic 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="/schema/beans"xmlns:xsi="/2001/XMLSchema-instance"xmlns:aop="/schema/aop"xsi:schemaLocation="/schema/beans/schema/beans/spring-beans.xsd/schema/aop/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="mons.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方法。
/*** 获取连接工具类,用于从连接池中获取一个连接,并且实现连接与线程绑定。*/@Componentpublic class ConnectionUtils {@Autowiredprivate ThreadLocal<Connection> tl;@Autowiredprivate 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// 使用该注解表明这是一个切面@Aspectpublic class TransactionManager {@Autowiredprivate 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 {@Autowiredprivate QueryRunner runner;@Autowiredprivate ConnectionUtils utils;@Overridepublic 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);}}@Overridepublic 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 {@Autowiredprivate AccountDao dao;@Overridepublic void updateAccount(Account account) {dao.updateAccount(account);}@Overridepublic 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// 使用该注解表明这是一个切面@Aspectpublic class TransactionManager {@Autowiredprivate 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/>