SpringAOP
1、入门案例
SpringAOP是spring的有一个核心的地方了,我觉得作为一种辅助工具是特别合适的。
通过一个业务场景来看下对应的使用场景以及利用springAOP所能够带来的好处。
最常见的就是银行转账的案例,所以我也来用这个例子来说明:
准备工作:JDK8+maven+Idea
controller层:
@RestController
@Slf4j
public class TransferController {
@Autowired
private TransferService transferService;
@RequestMapping("/transfer")
public Result result(@RequestBody TransferRequestDTO transferRequestDTO){
log.info("接收到的参数是:{}",transferRequestDTO.toString());
Integer flag = null;
try {
flag = this.transferService.transfer(transferRequestDTO);
if (flag>0){
return new Result(200,"转账成功",flag);
}
return new Result(300,"转账失败","金额不足");
} catch (Exception e) {
e.printStackTrace();
return new Result(300,"转账失败","用户名不存在");
}
}
}
service层:
@Service
@Slf4j
public class TransferService {
@Autowired
private TransferMapper transferMapper;
@Autowired
private TrasactionManager trasactionManager;
public Integer transfer(TransferRequestDTO transferRequestDTO) {
// 首先进行参数校验
if (transferRequestDTO == null) {
throw new RuntimeException("请求参数为空");
}
String from = transferRequestDTO.getFrom();
String to = transferRequestDTO.getTo();
if (from == null || to == null) {
throw new RuntimeException("无法查询到对应用户");
}
Double money = transferMapper.findMoneyByFrom(from);
Double dtoMoney = transferRequestDTO.getMoney();
if (money - dtoMoney >= 0) {
try {
trasactionManager.startTransaction();
transferMapper.transfer(from,-dtoMoney);
transferMapper.transfer(to,dtoMoney);
trasactionManager.commitAndClose();
} catch (Exception e) {
trasactionManager.rollbackAndClose();
log.error("转账出现问题{}",e);
}
return 1;
}
return -1;
}
}
mapper层:
@Mapper
public interface TransferMapper {
Double findMoneyByFrom(@Param("username") String username);
void transfer(@Param("username") String username, @Param("money") Double money);
}
entity层:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TransferRequestDTO {
private String from;
private String to;
private Double money;
}
utils层:
/**
* 操作数据库保证安全的时候需要保证使用的是同一个数据库连接
*/
@Component
public class TrasactionManager implements ApplicationContextAware {
private static final ThreadLocal<Connection> CONNECTION_THREAD_LOCAL = new ThreadLocal<>();
private DataSource dataSource;
/**
* 开启事务
*/
public void startTransaction() {
try {
Connection connection = dataSource.getConnection();
connection.setAutoCommit(false);
CONNECTION_THREAD_LOCAL.set(connection);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
/**
* 操作成功,提交并关闭
*/
public void commitAndClose() {
Connection connection = null;
try {
connection =CONNECTION_THREAD_LOCAL.get();
connection.commit();
connection.close();
CONNECTION_THREAD_LOCAL.remove();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
/**
* 操作失败,事务回滚
*/
public void rollbackAndClose() {
Connection connection = null;
try {
connection =CONNECTION_THREAD_LOCAL.get();
connection.rollback();
connection.close();
CONNECTION_THREAD_LOCAL.remove();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.dataSource = applicationContext.getBean(DataSource.class);
}
}
但是这样达不到对应的效果。因为尽管是同一个连接,但是没有回滚成功。
使用动态带来来实现,这里放上关键的代码:
@Component("accountServiceProxyFactory")
public class AccountServiceProxyFactory {
@Autowired
@Autowired
private TransactionManager txManager;
public AccountService createProxy(){
return (TransferService) Proxy.newProxyInstance(
transferService.getClass().getClassLoader(),
transferService.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
try {
//开启事务
txManager.startTransaction();
//调用目标对象的方法:执行转账
result = method.invoke(transferService, args);
//提交事务
txManager.commitAndClose();
} catch (Exception e) {
e.printStackTrace();
//回滚事务
txManager.rollbackAndClose();
}
return result;
}
}
); | }
}
可以看到由原始的
try {
trasactionManager.startTransaction();
transferMapper.transfer(from,-dtoMoney);
transferMapper.transfer(to,dtoMoney);
trasactionManager.commitAndClose();
} catch (Exception e) {
trasactionManager.rollbackAndClose();
log.error("转账出现问题{}",e);
}
这里的开启事务和关闭事务和这里的业务逻辑是没有关系的,但是仍然摆在了这里;
使用反射来进行改进:
try {
//开启事务
txManager.startTransaction();
//调用目标对象的方法:执行转账
result = method.invoke(transferService, args);
//提交事务
txManager.commitAndClose();
} catch (Exception e) {
e.printStackTrace();
//回滚事务
txManager.rollbackAndClose();
}
将业务逻辑中的代码写到了这里,这是一种非常可取的方式。所以spring中的AOP也是利用反射的方式来进行操作的。
动态代理的优势就在于不修改源码的情况下,进行功能增强。
还有一种最常见的使用方式:打印日志以及事务管理功能。
2、SpringAOP
2.1、简单概述
Spring的AOP,底层是通过动态代理实现的。在运行期间,通过代理技术动态生成代理对象,代理对象方法执行时进行功能的增强介入,再去调用目标方法,从而完成功能增强。 |
常用的动态代理技术有:
- JDK的动态代理:基于接口实现的
- cglib的动态代理:基于子类实现的
Spring的AOP采用了哪种代理方式?
- 如果目标对象有接口,就采用JDK的动态代理技术
- 如果目标对象没有接口,就采用cglib技术
我们通常使用的是JDK的代理方式。
2.2、相关概念
-
目标对象(Target):要代理的/要增强的目标对象。
-
代理对象(Proxy):目标对象被AOP织入增强后,就得到一个代理对象
-
连接点(JoinPoint):能够被拦截到的点,在Spring里指的是方法
目标类里,所有能够进行增强的方法,都是连接点
- 切入点(PointCut):要对哪些连接点进行拦截的定义
已经增强的连接点,叫切入点
- 通知/增强(Advice):拦截到连接点之后要做的事情
对目标对象的方法,进行功能增强的代码
-
切面(Aspect):是切入点和通知的结合
-
织入(Weaving):把增强/通知 应用到 目标对象来创建代理对象的过程。Spring采用动态代理技术织入,而AspectJ采用编译期织入和装载期织入
这些概念用一张图来进行总结一下:
代码是从上到下执行的,切面是横向拦截这些从上向下执行的代码。上面白色的是连接点,染红的是切点。
切面是一个通知组合起来的模块,通知是需要进行增强的代码,织入是将红色结点连接起来生成动态代理对象的方式。
明确两点原则:
在开发中我们需要做的事情:
1、编写出核心代码;
2、编写切面和通知;
3、将切点和通知连接起来;
Spring做的事情:
1、将切点和通知织入生成代理对象;
2、监控切点方法的执行,如果监控到了有对应的通知和切点结合,那么将会使用动态代理对象来执行对应的业务代码和通知方法;
2.3、切面和通知分类
看一个写好的版本:
xml配置版本的有点麻烦,所以直接使用注解版的。
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
//声明当前类是切面类:把切入点和通知,在这个类里进行织入,当前类就成为了一个切面类
@Aspect
@Component("myAdvice")
public class MyAdvice {
@Before("execution(void com.itheima.impl..*.save())")
public void before(){
System.out.println("前置通知...");
}
@AfterReturning("execution(void com.itheima.impl..*.save()))")
public void afterReturning(){
System.out.println("后置通知");
}
@After("execution(void com.itheima.impl..*.save())")
public void after(){
System.out.println("最终通知");
}
@AfterThrowing("execution(void com.itheima.impl..*.save())")
public void afterThrowing(){
System.out.println("抛出异常通知");
}
/**
* @param pjp ProceedingJoinPoint:正在执行的切入点方法对象。
* @return 切入点方法的返回值
*/
@Around("execution(void com.itheima.impl..*.save())")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("环绕:前置通知...");
//切入点方法执行
Object proceed = pjp.proceed();
System.out.println("环绕:后置通知...");
return proceed;
}
}
注意:在一个配置类上加上下面的注解
@EnableAspectJAutoProxy //开启AOP自动代理
通知的类型
名称 | 注解 | 说明 |
---|---|---|
前置通知 | @Before |
通知方法在切入点方法之前执行 |
后置通知 | @AfterRuturning |
通知方法在切入点方法之后执行 |
异常通知 | @AfterThrowing |
通知方法在抛出异常时执行 |
最终通知 | @After |
通知方法无论是否有异常,最终都执行 |
环绕通知 | @Around |
通知方法在切入点方法之前、之后都执行 |
切点表达式的抽取
- 同xml的AOP一样,当多个切面的切入点表达式相同时,可以将切入点表达式进行抽取;
- 抽取方法是:
- 在增强类(切面类,即被
@Aspect
标的类)上增加方法,在方法上使用@Pointcut
注解定义切入点表达式, - 在增强注解中引用切入点表达式所在的方法
@Aspect
@Component("myAdvice1")
public class MyAdvice1 {
//定义切入点表达式,目的是为了被其他引用而已
@Pointcut("execution(void com.itheima.service..*.save())")
public void myPointcut(){}
//引用切入点表达式
//完整写法:com.itheima.aop.MyAdvice.myPointcut()
//简单写法:myPointcut(), 引入当前类里定义的表达式,可以省略包类和类名不写
@Before("myPointcut()")
public void before(){
System.out.println("前置通知...");
}
@AfterReturning("myPointcut()")
public void afterReturning(){
System.out.println("后置通知");
}
@After("myPointcut()")
public void after(){
System.out.println("最终通知");
}
@AfterThrowing("myPointcut()")
public void afterThrowing(){
System.out.println("抛出异常通知");
}
/*@Around("myPointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("前置通知...");
//切入点方法执行
Object proceed = pjp.proceed();
System.out.println("后置通知..."); |
return proceed;
}*/
}
3、Spring的事务管理
首先来看下事务的传播行为和隔离级别
3.1、隔离级别
数据库中的知识点:
1、读未提交;----->脏读,读到了别的事务未提交的数据;
2、读已提交;----->不可重复读。针对的是某一行记录
3、可重复读;------>幻读------>针对的是一张表的记录
4、串行化;-------->事务一一实现,安全
MySQL的默认级别是第三种,可重复读;
3.2、传播行为
传播行为就是为了解决业务逻辑层中的方法调用方法的问题的。有七种方式,我就不在一一举例了,选择最常用的来进行讲解吧。
PROPAGATION_REQUIRED
:需要有事务,默认的(我们通常来使用的)。
如果有事务,就使用这个事务,如果没有事务,就创建事务。
3.3、事务超时和是否只读
在事务运行的时候,我们通常需要来设置事务的超时后事务自动回滚的情况。默认值是-1,表示的没有超时限制;如果,那么以秒为单位来进行设置;
对于事务来说,分为增删改查,那么针对于查的,设置为只读即可;
这里需要注意下失效的问题!
失效的问题是业务层方法调用业务层方法,然后对于这两个方法来说,两个事务不同的话那么就会有冲突。
有了冲突,就得来解决,使用谁的事务来解决冲突。
3.4、Demo
@Configuration
@ComponentScan("com.guang")
@EnableTransactionManagement // 自动byType注入事务管理器
public class AppConfig{
//连接池
@Bean
public DataSource dataSource() throws PropertyVetoException {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setDriverClass("com.mysql.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/jdbc");
dataSource.setUser("root");
dataSource.setPassword("root");
return dataSource;
}
//JdbcTemplate
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource){
return new JdbcTemplate(dataSource);
}
// 事务管理器,需要使用到连接池
// 还需要注意这里的实现有点不同:
// 如果你添加的是 spring-boot-starter-jdbc 依赖,框架会默认注入 DataSourceTransactionManager 实例。如果你添加的是 spring-boot-starter-data- // jpa 依赖,框架会默认注入 JpaTransactionManager 实例。
// 直接自己来进行指定,因为后期方便维护
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
return new DataSourceTransactionManager(dataSource);
}
}
``` |
对应的业务逻辑层:
```java
@Override
@Transactional(isolation = Isolation.DEFAULT, propagation = Propagation.REQUIRED, readOnly = false)
public void transfer(String from, String to, Double money) {
Account fromAccount = accountDao.findByName(from);
Account toAccount = accountDao.findByName(to);
fromAccount.setMoney(fromAccount.getMoney() - money);
toAccount.setMoney(toAccount.getMoney() + money);
accountDao.edit(fromAccount);
//int i = 1/0;
accountDao.edit(toAccount);
}
结果这里发现,我们就是利用了数据源配置了一个事务管理器,然后在核心方法上加了注解,就能够完成我们想要的效果,很明显,底层是通过动态代理的方式来实现的。