今日内容
- 能够理解AOP相关概念
- 能够说出AOP相关术语的含义能够应用AspectJ表达式语言
- 能够编写Spring的AOP中不同通知的代码
- 能够理解spring的AOP的注解
- 能够实现spring基于aop配置的事务控制案例
什么是AOP
AOP:Aspect Oriented Programming(面向切面编程/面向方面编程),AOP是OOP的一个延伸
- OOP(面向对象编程)
- 三大特征:封装、继承和多态
- 比如说,有DOG类、Cat类、Horse类,它们都有eat方法,run方法,eat打印了我能吃,run打印了我能跑,按照OOP的编程思想,那么我们可以抽象出父类Animal,在父类中放置相同的属性或者方法,这样来避免多子类中重复的代码。
- OOP是纵向抽取和继承体系,OOP很多场合都能够解决我们的问题,但是有一些场合它也处理不了解决不了代码重复的问题。
- 横切逻辑存在是存在问题
- 横切逻辑往往在很多方法中,它是重复的
- 横切逻辑和业务代码混杂在一起,让业务代码变得很臃肿,业务代码应该只处理业务逻辑
- OOP已经不能处理横切逻辑的问题了,AOP横空出世,AOP独辟蹊径的提出了横向抽取机制,将业务逻辑和横切逻辑分离,但分离不是难事,难的是分离之后怎么把横切逻辑再融合到原有业务逻辑上,达到和原来一样的效果
所以,我们要做的这个比较难的事就是在不修改(不侵犯)原有业务逻辑的基础上做逻辑增强(横切逻辑增强)。
为什么叫AOP(面向切面编程)
因为我们AOP要解决的问题就是在不改变业务逻辑基础上增强横切逻辑。在这个过程中,业务逻辑我们是不能改变的,所以我们不能面向业务逻辑,只能面向横切逻辑;另外还有一层意思,AOP要做增强的横切逻辑往往不是影响一个方法,往往会影响很多方法(对多处的业务逻辑进行增强),影响了一大片,有一个“面”的概念在里面,所以叫做面向切面编程。
AOP的实现方式
Spring的AOP的实现方式就是动态代理技术
AOP的优势
- 作用:程序运行期间,在不改变原有业务逻辑的情况下进行方法增强
- 优势:减少重复代码,提高开发效率,业务逻辑和增强的横切逻辑分离便于维护
APO的具体应用
案例:模拟转账(并且模拟转账异常)
- 汇款人账户减少一定的金额
- 收款人账户增加一定的金额
- 计算之后,更新数据库
- 问题:模拟转账异常(人为制造异常,在两次update之间造了异常)
问题:模拟转账异常(人为制造异常,在两次update之间造了异常)
- applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
">
<!-- 启动注解扫描-->
<context:component-scan base-package="com.itheima"></context:component-scan>
<!-- 配置dbcp连接池-->
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<!-- BasicDataSource成员注入-->
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/test"></property>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
</bean>
<!-- 配置QueryRunner-->
<bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner">
<!-- -构造方法注入-->
<constructor-arg name="ds" ref="dataSource"></constructor-arg>
</bean>
</beans>
- dao层
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {
@Autowired
@Qualifier("queryRunner")
private QueryRunner qr ;
/**
* 根据账户名查询账户
*/
public Account queryAccountByName(String name) throws SQLException {
String sql = "select id,name,money from account where name = ?";
return qr.query(sql,new BeanHandler<Account>(Account.class),name);
}
/**
* 保存转账后的金额
*/
public int updateAccountByName(Account account) throws SQLException {
String sql = "update account set name = ? , money = ? where id = ?";
return qr.update(sql,account.getName(),account.getMoney(),account.getId());
}
}
- service层
@Service("accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
@Qualifier("accountDao")
private AccountDao accountDao;
/**
* 根据收款人和付款人查询账户余额
* 计算金额
* 收款人余额 = 余额+转账金额
* 付款人余额 = 余额-转账金额
* 数据保存到数据库
*/
public void transfer(String fromName, String toName, double money)throws SQLException {
//查询付款人账户
Account accountFrom = accountDao.queryAccountByName(fromName);
//查询收款人账户
Account accountTo = accountDao.queryAccountByName(toName);
// 计算金额
accountFrom.setMoney( accountFrom.getMoney() - money );
accountTo.setMoney( accountTo.getMoney() + money);
//跟新数据库
accountDao.updateAccountByName(accountFrom);
int a= 1/0;
accountDao.updateAccountByName(accountTo);
}
}
- 测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class MainTest {
@Autowired
@Qualifier("accountService")
private AccountService service;
/**
* 测试转账
*/
@Test
public void testTransfer() throws SQLException {
service.transfer("张三","李四",100);
}
}
-
问题分析:
- 两次update使用了两个connection(这两个connection分别有自己的事务,而且事务是自动提交的)
- 换句话说,事务现在是在dao层的
-
问题处理解决
- 两次update得使用同一个连接
- 两次update属于service同一个方法,也就是在同一个线程内,那就把一个连接绑定到当前线程,两个update都用这个连接
- 创建ConnectionUtil类
-
ConnectionUtil类–线程和连接对象绑定
@Component("connectionUtil")
public class ConnectionUtil {
private ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();
@Autowired
@Qualifier("dataSource")
private BasicDataSource dataSource;
/**
* 返回当前线程绑定的connection对象
*/
public Connection getCurrentThreadConnection()throws SQLException{
Connection connection = threadLocal.get();
if(connection == null){
connection = dataSource.getConnection();
threadLocal.set(connection);
}
return connection;
}
/**
* 解除线程和connection对象的绑定
*/
public void remove(){
threadLocal.remove();
}
}
- TransactionManager–事务管理类
@Component("transactionManager")
public class TransactionManager {
@Autowired
@Qualifier("connectionUtil")
private ConnectionUtil connectionUtil ;
/**
* 开启事务,阻止事务自动提交
*/
public void begin() throws SQLException {
connectionUtil.getCurrentThreadConnection().setAutoCommit(false);
}
/**
* 提交事务,数据永久保存
*/
public void commit() throws SQLException {
connectionUtil.getCurrentThreadConnection().commit();
}
/**
* 回滚事务
*/
public void rollback() throws SQLException {
connectionUtil.getCurrentThreadConnection().rollback();
}
/**
* 归还连接池
*/
public void release() throws SQLException {
connectionUtil.getCurrentThreadConnection().close();
connectionUtil.remove();
}
}
- 业务层
@Service("accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
@Qualifier("accountDao")
private AccountDao accountDao;
@Autowired
@Qualifier("transactionManager")
private TransactionManager transactionManager;
/**
* 根据收款人和付款人查询账户余额
* 计算金额
* 收款人余额 = 余额+转账金额
* 付款人余额 = 余额-转账金额
* 数据保存到数据库
*/
public void transfer(String fromName, String toName, double money)throws SQLException {
try{
transactionManager.begin();
//转账业务逻辑
//查询付款人账户
Account accountFrom = accountDao.queryAccountByName(fromName);
//查询收款人账户
Account accountTo = accountDao.queryAccountByName(toName);
// 计算金额
accountFrom.setMoney( accountFrom.getMoney() - money );
accountTo.setMoney( accountTo.getMoney() + money);
//跟新数据库
accountDao.updateAccountByName(accountFrom);
//int a= 1/0;
accountDao.updateAccountByName(accountTo);
transactionManager.commit();
}catch (Exception ex){
ex.printStackTrace();
transactionManager.rollback();
}finally {
transactionManager.release();
}
}
}
我们不可能在每一个方法中添加tcf控制,因此引入动态代理技术来在不改变原有业务逻辑代码的基础上做逻辑增强。
- 动态代理工厂类
/**
* 动态代理工厂
* 代理业务层,在不改变业务层逻辑的情况下
* 增强原有业务层的功能(事务管理)
*/
@Component("proxyFactory")
public class ProxyFactory {
@Autowired
@Qualifier("transactionManager")
private TransactionManager transactionManager;
/**
* 返回代理对象
* 传递被代理对象(业务层)
*/
public Object getProxy(final Object object){
return Proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
try {
transactionManager.begin();
result = method.invoke(object, args);
transactionManager.commit();
}catch (Exception ex){
ex.printStackTrace();
transactionManager.rollback();
}finally {
transactionManager.release();
}
return result;
}
});
}
}
- 业务层改造
@Service("accountServiceImplNoTCF")
public class AccountServiceImplNoTCF implements AccountService {
@Autowired
@Qualifier("accountDao")
private AccountDao accountDao;
/**
* 根据收款人和付款人查询账户余额
* 计算金额
* 收款人余额 = 余额+转账金额
* 付款人余额 = 余额-转账金额
* 数据保存到数据库
*/
public void transfer(String fromName, String toName, double money)throws SQLException {
Account accountFrom = accountDao.queryAccountByName(fromName);
//查询收款人账户
Account accountTo = accountDao.queryAccountByName(toName);
// 计算金额
accountFrom.setMoney( accountFrom.getMoney() - money );
accountTo.setMoney( accountTo.getMoney() + money);
//跟新数据库
accountDao.updateAccountByName(accountFrom);
int a= 1/0;
accountDao.updateAccountByName(accountTo);
}
}
Spring中AOP使用
术语
-
Joinpoint(连接点)
- 横切程序执行的特定位置,比如类开始初始化前,类初始化之后,类中某个方法调用前、调用后,方法抛出异常后等,这些代码中的特定点就称为“连接点”。
- Spring仅支持方法的连接点,即仅能在方法调用前、方法调用后、方法抛出异常时以及方法调用前后这些程序执行点织入增强。
- 我们知道黑客攻击系统需要找到突破口,没有突破口就无法进行攻击,从这一角度上来说,AOP是一个黑客(因为它要向目前类中嵌入额外的代码逻辑),连接点就是AOP向目标类打入楔子的候选点。
-
Pointcut(切入点)
- 一个类中可以有很多个方法,每个方法又有多个Joinpoint,在这么多个方法中,如何定位到自己感兴趣的方法呢?靠的是切点
- 注意:切点只定位到某个方法上,所以如果希望定位到具体连接点上,还需要提供方位信息
- 比如:如果把一个方法理解成数据表中的一条记录的话,那么切入点就好比你select语句的where条件 ,就可以定位到你感兴趣的方法
-
Advice(通知/增强)
- 增强的第一层意思就是你的横切逻辑代码(增强逻辑代码)
- 在Spring中,增强除了用于描述横切逻辑外,包含一层意思就是横切逻辑执行的方位信息。刚刚说了切点只能定位到方法,在进一步使用方位信息就可以定位到我们感兴趣的连接点了(方法调用前、方法调用后还是方法抛出异常时等)。
-
Target(目标对象)
- 增强逻辑的织入目标类。比如未添加任何事务控制的AccountServiceImplNoTcf类
-
Weaving(织入)
- 织入是将增强逻辑/横切逻辑添加到目标类具体连接点上的过程,AOP像一台织布机,将目标类、增强或者引介通过AOP(其实就是动态代理技术)这台织布机天衣无缝地编织到一起。
-
Spring采用动态代理织入。
- Proxy(代理),一个类被AOP织入增强后,就产出了一个结果类,它是融合了原类和增强逻辑的代理类。
-
Aspect(切面)
- 切面由切点和增强(引介)组成。
- 切面=切点+增强
- =切点+方位信息+横切逻辑
- =连接点+横切逻辑
-
最终切面完成:把横切逻辑织入到哪些方法的方法前/后等
-
本质:把横切逻辑增强到连接点(切点和方位信息都是为了确定连接点)上
Spring关于JDK/CGLIB动态代理的选择
Spring发现涉及到接口那就使用JDK动态代理,如果不涉及接口就使用CGLIB动态代理
AOP:日志、性能监控、事务、权限控制
基于XML的AOP配置
-
需求:在Service层代码的不同方法的不同连接点JoinPoint织入日志
-
把Account表的service层进行crud模拟(dao层就不需要了)
-
引入POM坐标
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.9</version> </dependency>
-
-
applicationContext.xml配置
<aop:config>
所有aop配置的根标签<aop:aspect id="logAspect" ref="log">
配置切面对象,id自定义,ref横切逻辑类的bean标签id值<aop:pointcut>
配置切入点- 属性值:id自定义
- 属性值:expression 匹配方法的表达式
<aop:before>
被切入的方法前执行- 属性method:printBeforeMethod切入的方法名
- 属性pointcut-ref:切入点标签的id属性值
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
">
<context:component-scan base-package="com.itheima"></context:component-scan>
<!-- 横切逻辑配置-->
<bean id="log" class="com.itheima.utils.LogUtil">
</bean>
<!-- 配置aop的根标签-->
<aop:config>
<!-- 切面配置-->
<aop:aspect id="logAspect" ref="log">
<!--
配置切入点
before:方法执行之前
method:切入的方法名
pointcut:感兴趣的方法,被切入的方法
-->
<!--<aop:before method="printBeforeMethod"
pointcut="execution( int com.itheima.service.AccountService.saveAccount(com.itheima.pojo.Account))"></aop:before>-->
<!--
表达式配置
方法参数:* 匹配任意参数,必须有参数
.. 匹配任意参数,有无参数均可
方法名:*
返回值:*
-->
<aop:pointcut id="point1" expression="execution( * com.itheima.service.AccountService.*(..))"></aop:pointcut>
<aop:before method="printBeforeMethod" pointcut-ref="point1"></aop:before>
<aop:after-returning method="pringAfterReturn" pointcut-ref="point1"></aop:after-returning>
<aop:after-throwing method="printAfterThrowing" pointcut-ref="point1"></aop:after-throwing>
<aop:after method="printAfterMethod" pointcut-ref="point1"></aop:after>
</aop:aspect>
</aop:config>
</beans>
- 日志对象:横切逻辑
public class LogUtil {
//方法执行之前
public void printBeforeMethod(){
System.out.println("方法之前执行");
}
//方法执行之后打印
public void printAfterMethod(){
System.out.println("方法执行之后");
}
//方法异常执行打印
public void printAfterThrowing(){
System.out.println("方法异常时");
}
//方法正常执行打印
public void pringAfterReturn(){
System.out.println("方法正常时");
}
}
- service层
@Service("accountServiceImpl")
public class AccountServiceImpl implements AccountService {
public int saveAccount(Account account) {
System.out.println("模拟保存账户");
return 0;
}
@Override
public int updateAccountById(Account account) {
System.out.println("模拟更新账户");
return 0;
}
@Override
public int deleteAccountById(int id) {
System.out.println("模拟删除账户");
return 0;
}
@Override
public Account queryAccountById(int id) {
System.out.println("模拟查询账户");
return null;
}
}
- 测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class MainTest {
@Autowired
@Qualifier("accountServiceImpl")
private AccountService accountService;
@Test
public void testAop(){
accountService.saveAccount(new Account());
// accountService.queryAccountById(1);
}
}
环绕通知
它是spring为我们提供的一种可以在代码中手动控制增强方法何时执行的方式,灵活度比较高,设置可以控制原业务逻辑是否执行。
注意:通常情况下,环绕通知都是独立使用的,不要和上面的四种通知类型混合使用
-
<aop:around method="printRound" pointcut-ref="point1"></aop:around>
环绕通知配置 -
ProceedingJoinPoint进程切入点对象,执行我们的业务逻辑方法
- 方法:proceed() 执行我们自己的业务逻辑方法
-
applicationContext.xml配置
<aop:around method="printRound" pointcut-ref="point1"></aop:around>
- 日志对象:横切逻辑
//环绕通知
public Object printRound(ProceedingJoinPoint proceedingJoinPoint){
Object result = null;
try{
System.out.println("方法前执行");
result = proceedingJoinPoint.proceed();
System.out.println("方法正常执行");
}catch (Throwable ex){
ex.printStackTrace();
System.out.println("方法异常执行");
}
System.out.println("方法后执行");
return result;
}
注解形式实现AOP
半注解半xml形式
<!-- aop注解开关-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
- 横切逻辑对象
@Component("logUtil")
//横切逻辑对象
@Aspect
public class LogUtil {
//方法执行之前
@Before("execution(* com.itheima.service.AccountServiceImpl.*(..))")
public void printBeforeMethod(){
System.out.println("方法之前执行");
}
//方法执行之后打印
@After("execution(* com.itheima.service.AccountServiceImpl.*(..))")
public void printAfterMethod(){
System.out.println("方法执行之后");
}
@AfterThrowing("execution(* com.itheima.service.AccountServiceImpl.*(..))")
//方法异常执行打印
public void printAfterThrowing(){
System.out.println("方法异常时");
}
@AfterReturning("execution(* com.itheima.service.AccountServiceImpl.*(..))")
//方法正常执行打印
public void pringAfterReturn(){
System.out.println("方法正常时");
}
}
- 测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class MainTest {
@Autowired
@Qualifier("accountServiceImpl")
private AccountService accountService;
@Test
public void testAop(){
accountService.saveAccount( new Account());
}
}
- 横切逻辑对象抽取
@Component("logUtil")
//横切逻辑对象
@Aspect
public class LogUtil {
@Pointcut("execution(* com.itheima.service.AccountServiceImpl.*(..))")
public void point(){
}
//方法执行之前
@Before("point()")
public void printBeforeMethod(){
System.out.println("方法之前执行");
}
//方法执行之后打印
@After("point()")
public void printAfterMethod(){
System.out.println("方法执行之后");
}
@AfterThrowing("point()")
//方法异常执行打印
public void printAfterThrowing(){
System.out.println("方法异常时");
}
@AfterReturning("point()")
//方法正常执行打印
public void pringAfterReturn(){
System.out.println("方法正常时");
}
}
- 环绕通知
//环绕通知
@Around("point()")
public Object printRound(ProceedingJoinPoint proceedingJoinPoint){
Object result = null;
try{
System.out.println("方法前执行");
result = proceedingJoinPoint.proceed();
System.out.println("方法正常执行");
}catch (Throwable ex){
ex.printStackTrace();
System.out.println("方法异常执行");
}
System.out.println("方法后执行");
return result;
}
纯注解形式实现AOP
- 创建框架启动配置类
@Component
@ComponentScan("com.itheima")
//启用动态代理
@EnableAspectJAutoProxy
public class SpringConfig {
}
- 测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class MainTest {
@Autowired
@Qualifier("accountServiceImpl")
private AccountService accountService;
@Test
public void testAop(){
accountService.saveAccount( new Account());
}
}