AOP的基本原理和概念请看JAVA–AOP
AOP的相关术语
- AOP相关的术语
- Joinpoint(连接点):
所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点(我的理解是目标类的所有方法) - Pointcut(切入点):
所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。 - Advice(通知/增强):
所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知(动态代理中的功能增强)
通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。 - Introduction(引介):
引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方 法或 Field。 - Target(目标对象):
代理的目标对象 - Weaving(织入):
是指把增强应用到目标对象来创建新的代理对象的过程(增强后的对象)
spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入 - Proxy(代理):
一个类被 AOP 织入增强后,就产生一个结果代理类 - Aspect(切面):
是切入点和通知(引介)的结合
- Joinpoint(连接点):
Spring 中的 AOP 要明确的事
- 开发阶段:
开发人员只需把业务(服务)层中重复的代码抽离出来制成通知类,用xml配置文件或者注解的方式声明切入点和通知间的关系(就是被拦截方法要执行的通知方法),即切面即可。 - 运行阶段:
Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对 象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行 - 代理模式的选择:
在 Spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式
基于Aspectj 通过XML的AOP配置事务
普通通知(和环绕通知互斥使用)
- 具体案例(通过注解的方式配置Spring实现银行的curd操作),省略了持久层的接口、服务层的接口、全局配置类、数据库配置类、maven大部分依赖(spring-context、spring-test、c3p0、junit、dbutils、mysql、lombok)
- 添加切点表达式依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
- 创建银行账户实体类Account
@Data
public class Account implements Serializable {
private Integer id;
private String name;
private Float money;
}
- 创建持久层接口实现类AccountDaoImpl
@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
@Autowired
private QueryRunner runner;
@Autowired
private ConnectionUtils connectionUtils;
@Override
public List<Account> findAllAccount() {
try {
return runner.query(connectionUtils.getThreadConnection(),"select * from account",new BeanListHandler<Account>(Account.class));
}catch (Exception e){
throw new RuntimeException(e);
}
}
@Override
public Account findAccountById(Integer accountId) {
try {
return runner.query(connectionUtils.getThreadConnection(),"select * from account where id = ?",new BeanHandler<Account>(Account.class),accountId);
}catch (Exception e){
throw new RuntimeException(e);
}
}
@Override
public void saveAccount(Account account) {
try{
runner.update(connectionUtils.getThreadConnection(),"insert into account(name,money)values(?,?)",account.getName(),account.getMoney());
}catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void updateAccount(Account account) {
try{
runner.update(connectionUtils.getThreadConnection(),"update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
}catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void deleteAccount(Integer accountId) {
try{
runner.update(connectionUtils.getThreadConnection(),"delete from account where id=?",accountId);
}catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public Account findAccountByName(String accountName) {
try{
List<Account> accounts = runner.query(connectionUtils.getThreadConnection(),"select * from account where name = ? ",new BeanListHandler<Account>(Account.class),accountName);
if(accounts == null || accounts.size() == 0){
return null;
}
if(accounts.size() > 1){
throw new RuntimeException("结果集不唯一,数据有问题");
}
return accounts.get(0);
}catch (Exception e) {
throw new RuntimeException(e);
}
}
}
- 创建服务层接口实现类AccountServiceImpl(位置有限只写了2个方法)
@Service("accountService")
public class AccountServiceImpl implements IAccountService {
@Autowired
private IAccountDao accountDao;
@Autowired
private TransactionManager transactionManager;
@Override
public void saveAccount(Account account) {
try {
// 开启事务
txManager.beginTransaction();
// 执行操作
accountDao.saveAccount(account);
// 提交事务
txManager.commit();
}catch (Exception e){
// 回滚操作
txManager.rollback();
}finally {
// 释放连接
txManager.release();
}
}
@Override
public void transfer(String sourceName, String targetName, Float money) {
try {
// 开启事务
transactionManager.beginTransaction();
// 执行操作,根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
// 根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
// 转出账户减钱
source.setMoney(source.getMoney() - money);
// 转入账户加钱
target.setMoney(target.getMoney() + money);
// 更新转出账户
accountDao.updateAccount(source);
// 假设转账异常
//int i=1/0;
// 更新转入账户
accountDao.updateAccount(target);
// 提交事务
transactionManager.commit();
} catch (Exception e) {
// 回滚操作
transactionManager.rollback();
e.printStackTrace();
} finally {
// 释放连接
transactionManager.release();
}
}
}
- 创建连接工具类ConnectionUtils,因为在多例模式下,所有通过连接和线程绑定来控制事务(控制转账操作为一条连接)
@Component
public class ConnectionUtils {
private ThreadLocal<Connection> t = new ThreadLocal<Connection>();
@Autowired
private DataSource dataSource;
public Connection getThreadConnection(){
try{
//从ThreadLocal上获取连接
Connection con = t.get();
// 判断是否有连接
if (con == null){
// 从数据源中获取一个连接,并且存入ThreadLocal中
con = dataSource.getConnection();
t.set(con);
}
return con;
}catch (Exception e){
throw new RuntimeException(e);
}
}
// 把连接和线程解绑
public void removeConnection(){
t.remove();
}
}
- 创建事务管理类TransactionManager
@Component
public class TransactionManager {
@Autowired
private ConnectionUtils connectionUtils;
// 开启事务
public void beginTransaction(){
try {
System.out.println("前置");
connectionUtils.getThreadConnection().setAutoCommit(false);
}catch (Exception e){
e.printStackTrace();
}
}
// 提交事务
public void commit(){
try {
System.out.println("后置");
connectionUtils.getThreadConnection().commit();
}catch (Exception e){
e.printStackTrace();
}
}
// 回滚事务
public void rollback(){
try {
System.out.println("异常");
connectionUtils.getThreadConnection().rollback();
}catch (Exception e){
e.printStackTrace();
}
}
// 释放连接
public void release(){
try {
System.out.println("最终");
connectionUtils.getThreadConnection().close();//还回连接池中
connectionUtils.removeConnection();
}catch (Exception e){
e.printStackTrace();
}
}
}
- 配置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"
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">
<!--配置aop-->
<aop:config>
<!--配置切面-->
<aop:aspect id="manage" ref="transactionManager">
<aop:pointcut id="p" expression="execution(* com.zsc.service.impl.*.*(..))"/>
<!--前置通知,在切入点方法执行前执行-->
<aop:before method="beginTransaction" pointcut-ref="p"/>
<!--后置通知,在切入点方法执行后执行-->
<aop:after-returning method="commit" pointcut-ref="p"/>
<!--异常通知,在切入点方法执行异常时执行-->
<aop:after-throwing method="rollback" pointcut-ref="p"/>
<!--最终通知,不管方法执行是否异常都会执行-->
<aop:after method="release" pointcut-ref="p"/>
</aop:aspect>
</aop:config>
</beans>
- XML配置标签解释
spring基于xml的AOP配置步骤(把通知bean交给spring管理):
1.使用aop:config标签表明开始AOP的配置
2.使用aop:aspect标签表明配置切面
属性:
id:是切面的唯一标识符,可以随便取,只有唯一就行
ref:指定通知(功能增强)类bean的id
3.在aop:aspect标签内部使用对应标签来配置通知的的类型(简单来讲就是执行方法的时间)
1).aop:before标签表示前置通知,在切入点方法(要功能增强的方法)执行之前执行
2).aop:after-returning标签表示后置通知,在切入点方法(要功能增强的方法)执行之后执行
3).aop:after-throwing标签表示异常通知,在方法执行出现异常时执行,和上面的后置通知互斥
4).aop:after标签表示最终通知,不管方法执行是否异常都会执行
======================================分割线=========================================
5).aop:around标签表示环绕通知,需要在通知类中配置通知循序
上面标签都有的属性:
method:用于指定通知类的方法(每个通知要执行的方法)
pointcut:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
pointcut-ref:指定aop:pointcut标签里的切入点表达式
- 切入点表达式解释
切入点表达式写法:
关键字:execution
表达式:
访问修饰符 返回值类型包名.包名(包名有多少层写多少层).类名.方法名(参数列表)
标准表达式:
public void com.zsc.service.impl.AccountServiceImpl.transferByProxy(String sourceName, String targetName, Float money)
访问修饰符可以省略:
void com.zsc.service.impl.AccountServiceImpl.transferByProxy(String sourceName, String targetName, Float money)
返回值类型可以使用通配符,表示任意返回值类型:
* com.zsc.service.impl.AccountServiceImpl.transferByProxy(String sourceName, String targetName, Float money)
包名可以使用通配符,有几层写几个*:
* *.*.*.*.AccountServiceImpl.transferByProxy(String sourceName, String targetName, Float money)
包名也可以使用..表示当前包及子包:
* *..AccountServiceImpl.transferByProxy(String sourceName, String targetName, Float money)
类名、方法名都可以使用通配符表示:
* *..*.*(String sourceName, String targetName, Float money)
参数列表:
可以直接写参数类型名称(比如int)
引用类型写包名.类名的方式(比如java.lang.String)
可以使用通配符表示任意类型,但是必须有参数
可以使用..表示有无参数均可,有参数可以是任意类型
全通配写法:
* *..*.*(..)
实际开发中通常写法:
通配符返回值类型 包名.包名.包名.包名.通配符类名.通配符方法名(..可有可无参数列表)
* com.zsc.service.impl.*.*(..)
环绕通知(和上面的普通通知互斥,但标签属性一样)
- XML配置
<aop:config>
<!--配置切面-->
<aop:aspect id="manage" ref="transactionManager">
<aop:pointcut id="p" expression="execution(* com.zsc.service.impl.*.*(..))"/>
<aop:around method="aroundPrint" pointcut-ref="p"/>
</aop:aspect>
</aop:config>
- 在事务管理类TransactionManager添加环绕方法aroundPrint
public Object aroundPrint(ProceedingJoinPoint point){
try {
Object[] args = point.getArgs();
beginTransaction();
Object revalue = point.proceed(args);
commit();
return revalue;
}catch (Throwable t){
rollback();
throw new RuntimeException(t);
}finally {
release();
}
}
- 基于XML环绕通知解释
- 环绕通知和上面的通知要互斥使用
- 如果配置了xml没有写通知方法,切入点方法是不会运行的,而通知会运行
- 对比动态代理,因为只配置xml没有明确的切入点方法调用,所以切入点方法没有执行
- ProceedingJoinPoint接口有proceed()方法,此方法就相当于调用明确的切入点方法
- 若用注解的方式配置环绕通知,那么其他通知方法不要用注解,不然会执行2次
基于Aspectj 通过注解的AOP配置事务
普通通知
- 在XML配置文件中用< aop:aspectj-autoproxy />标签开启基于注解的AOP配置或在配置类中用@EnableAspectJAutoProxy注解开启基于注解的AOP配置
- 改造事务管理类TransactionManager
@Component
// 声明为切面类
@Aspect
public class TransactionManager {
@Autowired
private ConnectionUtils connectionUtils;
// 切入点表达式
@Pointcut("execution(* com.zsc.service.impl.*.transferByAOP(..))")
private void p() {}
// 开启事务
@Before("p()")
public void beginTransaction(){
try {
System.out.println("前置");
connectionUtils.getThreadConnection().setAutoCommit(false);
}catch (Exception e){
e.printStackTrace();
}
}
// 提交事务
@AfterReturning("p()")
public void commit(){
try {
System.out.println("后置");
connectionUtils.getThreadConnection().commit();
}catch (Exception e){
e.printStackTrace();
}
}
// 回滚事务
@AfterThrowing("p()")
public void rollback(){
try {
System.out.println("异常");
connectionUtils.getThreadConnection().rollback();
}catch (Exception e){
e.printStackTrace();
}
}
// 释放连接
@After("p()")
public void release(){
try {
System.out.println("最终");
connectionUtils.getThreadConnection().close();//还回连接池中
connectionUtils.removeConnection();
}catch (Exception e){
e.printStackTrace();
}
}
}
- 基于注解的普通通知说明
在使用spring框架版本为5.0.2.RELEASE时基于注解的通知的顺序出错,但在使用5.2.9.RELEASE版本是正确,暂时还没知道哪个版本修复了这个bug
环绕通知
- 改造事务管理类TransactionManager
@Component
// 声明为切面类
@Aspect
public class TransactionManager {
@Autowired
private ConnectionUtils connectionUtils;
// 切入点表达式
@Pointcut("execution(* com.zsc.service.impl.*.transferByAOP(..))")
private void p() {}
// 开启事务
public void beginTransaction(){
try {
System.out.println("前置");
connectionUtils.getThreadConnection().setAutoCommit(false);
}catch (Exception e){
e.printStackTrace();
}
}
// 提交事务
public void commit(){
try {
System.out.println("后置");
connectionUtils.getThreadConnection().commit();
}catch (Exception e){
e.printStackTrace();
}
}
// 回滚事务
public void rollback(){
try {
System.out.println("异常");
connectionUtils.getThreadConnection().rollback();
}catch (Exception e){
e.printStackTrace();
}
}
// 释放连接
public void release(){
try {
System.out.println("最终");
connectionUtils.getThreadConnection().close();//还回连接池中
connectionUtils.removeConnection();
}catch (Exception e){
e.printStackTrace();
}
}
@Around("p()")
public Object aroundPrint(ProceedingJoinPoint point){
try {
Object[] args = point.getArgs();
beginTransaction();
Object revalue = point.proceed(args);
commit();
return revalue;
}catch (Throwable t){
rollback();
throw new RuntimeException(t);
}finally {
release();
}
}
}
- 基于注解的环绕通知说明
在使用环绕通知时,普通通知的方法的通知注解应该删掉或注释,否则通知会执行2次
我对基于Spring框架的AOP理解
- Spring会帮我们管理通知,无需自己编写动态代理类,开发者只需通过注解或配置XML文件的方式,去规定通知和切入点方法的关系即可