引子

我们有一个转账需求
下面是示例代码

  1. public class Account {
  2. private Integer id;
  3. private String name;
  4. private Double money;
  5. // setter getter....
  6. }
  7. public interface AccountDao {
  8. // 转出操作
  9. public void out(String outUser, Double money);
  10. // 转入操作
  11. public void in(String inUser, Double money);
  12. }
  13. @Repository
  14. public class AccountDaoImpl implements AccountDao {
  15. @Autowired
  16. private QueryRunner queryRunner;
  17. @Override
  18. public void out(String outUser, Double money) {
  19. try {
  20. queryRunner.update("update account set money=money-? where name=?",
  21. money, outUser);
  22. } catch (SQLException e) {
  23. e.printStackTrace();
  24. }
  25. }
  26. @Override
  27. public void in(String inUser, Double money) {
  28. try {
  29. queryRunner.update("update account set money=money+? where name=?",
  30. money, inUser);
  31. } catch (SQLException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. }
  36. public interface AccountService {
  37. public void transfer(String outUser, String inUser, Double money);
  38. }
  39. @Service
  40. public class AccountServiceImpl implements AccountService {
  41. @Autowired
  42. private AccountDao accountDao;
  43. @Override
  44. public void transfer(String outUser, String inUser, Double money) {
  45. accountDao.out(outUser, money);
  46. accountDao.in(inUser, money);
  47. }
  48. }

核心配置文件如下

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:context="http://www.springframework.org/schema/context"
  5. xsi:schemaLocation="
  6. http://www.springframework.org/schema/beans
  7. http://www.springframework.org/schema/beans/spring-beans.xsd
  8. http://www.springframework.org/schema/context
  9. http://www.springframework.org/schema/context/spring-context.xsd">
  10. <!--开启组件扫描-->
  11. <context:component-scan base-package="com.lagou"/>
  12. <!--加载jdbc配置文件-->
  13. <context:property-placeholder location="classpath:jdbc.properties"/>
  14. <!--把数据库连接池交给IOC容器-->
  15. <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
  16. <property name="driverClassName" value="${jdbc.driver}"></property>
  17. <property name="url" value="${jdbc.url}"></property>
  18. <property name="username" value="${jdbc.username}"></property>
  19. <property name="password" value="${jdbc.password}"></property>
  20. </bean>
  21. <!--把QueryRunner交给IOC容器-->
  22. <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner">
  23. <constructor-arg name="ds" ref="dataSource"></constructor-arg>
  24. </bean>
  25. </beans>
  1. 测试代码如下
  1. @RunWith(SpringJUnit4ClassRunner.class)
  2. @ContextConfiguration("classpath:applicationContext.xml")
  3. public class AccountServiceTest {
  4. @Autowired
  5. private AccountService accountService;
  6. @Test
  7. public void testTransfer() throws Exception {
  8. accountService.transfer("tom", "jerry", 100d);
  9. }
  10. }

问题

上面的代码事务在dao层,转出转入操作都是一个独立的事务,但实际开发,应该把业务逻辑控制在 一个事务中,所以应该将事务挪到service层。

传统事务解决方法

  1. 编写线程绑定工具类
  2. 编写事务管理器
  3. 修改service层代码
  4. 修改dao层代码
  1. @Component
  2. public class ConnectionUtils {
  3. private ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
  4. @Autowired
  5. private DataSource dataSource;
  6. /**
  7. * 获取当前线程上的连接
  8. *
  9. * @return Connection
  10. */
  11. public Connection getThreadConnection() {
  12. // 1.先从ThreadLocal上获取
  13. Connection connection = threadLocal.get();
  14. // 2.判断当前线程是否有连接
  15. if (connection == null) {
  16. try {
  17. // 3.从数据源中获取一个连接,并存入到ThreadLocal中
  18. connection = dataSource.getConnection();
  19. threadLocal.set(connection);
  20. } catch (SQLException e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. return connection;
  25. }
  26. /**
  27. * 解除当前线程的连接绑定
  28. */
  29. public void removeThreadConnection() {
  30. threadLocal.remove();
  31. }
  32. }
  1. /**
  2. * 事务管理器工具类,包含:开启事务、提交事务、回滚事务、释放资源
  3. */
  4. @Component
  5. public class TransactionManager {
  6. @Autowired
  7. private ConnectionUtils connectionUtils;
  8. public void beginTransaction() {
  9. try {
  10. connectionUtils.getThreadConnection().setAutoCommit(false);
  11. } catch (SQLException e) {
  12. e.printStackTrace();
  13. }
  14. }
  15. public void commit() {
  16. try {
  17. connectionUtils.getThreadConnection().commit();
  18. } catch (SQLException e) {
  19. e.printStackTrace();
  20. }
  21. }
  22. public void rollback() {
  23. try {
  24. connectionUtils.getThreadConnection().rollback();
  25. } catch (SQLException e) {
  26. e.printStackTrace();
  27. }
  28. }
  29. public void release() {
  30. try {
  31. connectionUtils.getThreadConnection().setAutoCommit(true); // 改回自
  32. 动提交事务
  33. connectionUtils.getThreadConnection().close();// 归还到连接池
  34. connectionUtils.removeThreadConnection();// 解除线程绑定
  35. } catch (SQLException e) {
  36. e.printStackTrace();
  37. }
  38. }
  39. }

在service层里调用

  1. @Service
  2. public class AccountServiceImpl implements AccountService {
  3. @Autowired
  4. private AccountDao accountDao;
  5. @Autowired
  6. private TransactionManager transactionManager;
  7. @Override
  8. public void transfer(String outUser, String inUser, Double money) {
  9. try {
  10. // 1.开启事务
  11. transactionManager.beginTransaction();
  12. // 2.业务操作
  13. accountDao.out(outUser, money);
  14. int i = 1 / 0;
  15. accountDao.in(inUser, money);
  16. // 3.提交事务
  17. transactionManager.commit();
  18. } catch (Exception e) {
  19. e.printStackTrace();
  20. // 4.回滚事务
  21. transactionManager.rollback();
  22. } finally {
  23. // 5.释放资源
  24. transactionManager.release();
  25. }
  26. }
  27. }
  1. 最后再回头修改一下Dao
  1. @Repository
  2. public class AccountDaoImpl implements AccountDao {
  3. @Autowired
  4. private QueryRunner queryRunner;
  5. @Autowired
  6. private ConnectionUtils connectionUtils;
  7. @Override
  8. public void out(String outUser, Double money) {
  9. try {
  10. queryRunner.update(connectionUtils.getThreadConnection(), "update
  11. account set money=money-? where name=?", money, outUser);
  12. } catch (SQLException e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. @Override
  17. public void in(String inUser, Double money) {
  18. try {
  19. queryRunner.update(connectionUtils.getThreadConnection(), "update
  20. account set money=money+? where name=?", money, inUser);
  21. } catch (SQLException e) {
  22. e.printStackTrace();
  23. }
  24. }
  25. }

上面代码,通过对业务层改造,已经可以实现事务控制了,但是由于我们添加了事务控制,也产生了 一个新的问题: 业务层方法变得臃肿了,里面充斥着很多重复代码。并且业务层方法和事务控制方法耦 合了,违背了面向对象的开发思想。

使用动态代理优化

我们可以将业务代码和事务代码进行拆分,通过动态代理的方式,对业务方法进行事务的增强。这样 就不会对业务层产生影响,解决了耦合性的问题啦!
常用的动态代理技术
JDK 代理 :基于接口的动态代理技术,利用拦截器(必须实现invocationHandler)加上反射机制生成 一个代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理,从而实现方法增强
CGLIB代理:基于父类的动态代理技术:动态生成一个要代理的子类,子类重写要代理的类的所有不是 final的方法。在子类中采用方法拦截技术拦截所有的父类方法的调用,顺势织入横切逻辑,对方法进行增强
image.png

JDK动态代理

  1. @Component
  2. public class JDKProxyFactory {
  3. @Autowired
  4. private AccountService accountService;
  5. @Autowired
  6. private TransactionManager transactionManager;
  7. public AccountService createAccountServiceJDKProxy() {
  8. AccountService accountServiceProxy = (AccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
  9. accountService.getClass().getInterfaces(), new InvocationHandler() {
  10. @Override
  11. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  12. try {
  13. transactionManager.beginTransaction();
  14. method.invoke(accountService, args);
  15. transactionManager.commit();
  16. } catch (Exception e) {
  17. e.printStackTrace();
  18. transactionManager.rollBack();
  19. } finally {
  20. transactionManager.release();
  21. }
  22. return null;
  23. }
  24. });
  25. return accountServiceProxy;
  26. }
  27. }
  28. @Service(value = "accountService")
  29. public class AccountServiceImpl implements AccountService {
  30. @Autowired
  31. private AccountDao accountDao;
  32. @Override
  33. public void transfer(String outUser, String inUser, double money) {
  34. accountDao.out(outUser, money);
  35. accountDao.in(inUser, money);
  36. }
  37. }

Cglib动态代理

  1. @Component
  2. public class CglibProxyFactory {
  3. @Autowired
  4. private AccountService accountService;
  5. @Autowired
  6. private TransactionManager transactionManager;
  7. public AccountService createAccountServiceCglibProxy() {
  8. AccountService accountServiceProxy = null;
  9. /*
  10. * 参数一:目标对象的字节码对象
  11. * 参数二:动作类,实现增强功能
  12. * */
  13. accountServiceProxy = (AccountService)
  14. Enhancer.create(accountService.getClass(), new MethodInterceptor() {
  15. @Override
  16. public Object intercept(Object o, Method method, Object[] objects,
  17. MethodProxy methodProxy) throws Throwable {
  18. Object result = null;
  19. try {
  20. // 1.开启事务
  21. transactionManager.beginTransaction();
  22. // 2.业务操作
  23. result = method.invoke(accountService, objects);
  24. // 3.提交事务
  25. transactionManager.commit();
  26. } catch (Exception e) {
  27. e.printStackTrace();
  28. // 4.回滚事务
  29. transactionManager.rollback();
  30. } finally {
  31. // 5.释放资源
  32. transactionManager.release();
  33. }
  34. return result;
  35. }
  36. });
  37. return accountServiceProxy;
  38. }
  39. }

AOP

AOP 为 Aspect Oriented Programming 的缩写,意思为面向切面编程
AOP 是 OOP(面向对象编程) 的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
这样做的好处是:

  1. 在程序运行期间,在不修改源码的情况下对方法进行功能增强
  2. 逻辑清晰,开发核心业务的时候,不必关注增强业务的代码
  3. 减少重复代码,提高开发效率,便于后期维护

实际上,AOP 的底层是通过 Spring 提供的的动态代理技术实现的。在运行期间,Spring通过动态代理技术动态的生成代理对象,代理对象方法执行时进行增强功能的介入,在去调用目标对象的方法,从而完成功能的增强。

相关术语

  • Target(目标对象):代理的目标对象
  • Proxy (代理):一个类被 AOP 织入增强后,就产生一个结果代理类
  • Joinpoint(连接点):所谓连接点是指那些可以被拦截到的点。在spring中指的是方法,因为
    spring只支持方法类型的连接点
  • Pointcut(切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义
  • Advice(通知/ 增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知
    分类:前置通知、后置通知、异常通知、最终通知、环绕通知
  • Aspect(切面):是切入点和通知(引介)的结合
  • Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织
    入,而AspectJ采用编译期织入和类装载期织入

注意事项

在 Spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。 当bean实现接口时,会用JDK代理模式 当bean没有实现接口,用cglib实现( 可以强制使用cglib(在spring配置中加入

  1. <aop:aspectj-autoproxy proxyt-target-class="true"/>

))

基于XML的AOP开发

  1. 创建java项目,导入AOP相关坐标
  2. 创建目标接口和目标实现类(定义切入点)
  3. 创建通知类及方法(定义通知)
  4. 将目标类和通知类对象创建权交给spring
  5. 在核心配置文件中配置织入关系,及切面
  6. 编写测试代码
  1. <dependencies>
  2. <!--导入spring的context坐标,context依赖aop-->
  3. <dependency>
  4. <groupId>org.springframework</groupId>
  5. <artifactId>spring-context</artifactId>
  6. <version>5.1.5.RELEASE</version>
  7. </dependency>
  8. <!-- aspectj的织入(切点表达式需要用到该jar包) -->
  9. <dependency>
  10. <groupId>org.aspectj</groupId>
  11. <artifactId>aspectjweaver</artifactId>
  12. <version>1.8.13</version>
  13. </dependency>
  14. <!--spring整合junit-->
  15. <dependency>
  16. <groupId>org.springframework</groupId>
  17. <artifactId>spring-test</artifactId>
  18. <version>5.1.5.RELEASE</version>
  19. </dependency>
  20. <dependency>
  21. <groupId>junit</groupId>
  22. <artifactId>junit</artifactId>
  23. <version>4.12</version>
  24. </dependency>
  25. </dependencies>
  1. public interface AccountService {
  2. public void transfer();
  3. }
  4. public class AccountServiceImpl implements AccountService {
  5. @Override
  6. public void transfer() {
  7. System.out.println("转账业务...");
  8. }
  9. }
  10. public class MyAdvice {
  11. public void before() {
  12. System.out.println("前置通知...");
  13. }
  14. }
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:aop="http://www.springframework.org/schema/aop"
  5. xsi:schemaLocation="http://www.springframework.org/schema/beans
  6. http://www.springframework.org/schema/beans/spring-beans.xsd
  7. http://www.springframework.org/schema/aop
  8. http://www.springframework.org/schema/aop/spring-aop.xsd">
  9. <bean id="accountService" class="com.ning.service.impl.AccountServiceImpl"/>
  10. <bean id="myAdvice" class="com.ning.advice.MyAdvice"/>
  11. <aop:config>
  12. <aop:aspect ref="myAdvice">
  13. <aop:before method="before" pointcut="execution(public void com.ning.service.impl.AccountServiceImpl.transfer())"/>
  14. </aop:aspect>
  15. </aop:config>
  16. </beans>
  1. @RunWith(SpringJUnit4ClassRunner.class)
  2. @ContextConfiguration("classpath:applicationContext.xml")
  3. class AccountServiceTest {
  4. @Autowired
  5. private AccountService accountService;
  6. @Test
  7. public void testTransfer() throws Exception {
  8. accountService.transfer();
  9. }
  10. }

XML Execution表达式语法

  1. execution([修饰符] 返回值类型 包名.类名.方法名(参数))
  2. 访问修饰符可以省略
  3. 返回值类型、包名、类名、方法名可以使用星号 * 代替,代表任意
  4. 包名与类名之间一个点 . 代表当前包下的类,两个点 .. 表示当前包及其子包下的类
  5. 参数列表可以使用两个点 .. 表示任意个数,任意类型的参数列表
  6. execution(public void com.lagou.service.impl.AccountServiceImpl.transfer())
  7. execution(void com.lagou.service.impl.AccountServiceImpl.*(..))
  8. execution(* com.lagou.service.impl.*.*(..))
  9. execution(* com.lagou.service..*.*(..))

当多个增强的切点表达式相同时,可以将切点表达式进行抽取,在增强中使用 pointcut-ref 属性代替 pointcut 属性来引用抽取后的切点表达式。

  1. <aop:config>
  2. <!--抽取的切点表达式-->
  3. <aop:pointcut id="myPointcut" expression="execution(* com.lagou.service..*.*
  4. (..))"> </aop:pointcut>
  5. <aop:aspect ref="myAdvice">
  6. <aop:before method="before" pointcut-ref="myPointcut"></aop:before>
  7. </aop:aspect>
  8. </aop:config>

通知语法

  1. <aop:通知类型 method=“通知类中方法名” pointcut=“切点表达式"></aop:通知类型>

image.png
注意:通常情况下,环绕通知都是独立使用的
环绕通知的使用方法

  1. public Object around(ProceedingJoinPoint joinPoint) {
  2. try {
  3. System.out.println("前置通知执行了");
  4. final Object o = joinPoint.proceed();
  5. System.out.println("后置通知执行了");
  6. return o;
  7. } catch (Throwable throwable) {
  8. throwable.printStackTrace();
  9. System.out.println("异常通知执行了");
  10. } finally {
  11. System.out.println("最终通知执行了");
  12. }
  13. return null;
  14. }

基于注解的AOP开发

依赖

  1. <dependencies>
  2. <!--导入springcontext坐标,context依赖aop-->
  3. <dependency>
  4. <groupId>org.springframework</groupId>
  5. <artifactId>spring-context</artifactId>
  6. <version>5.1.5.RELEASE</version>
  7. </dependency>
  8. <!-- aspectj的织入 -->
  9. <dependency>
  10. <groupId>org.aspectj</groupId>
  11. <artifactId>aspectjweaver</artifactId>
  12. <version>1.8.13</version>
  13. </dependency>
  14. <!--spring整合junit-->
  15. <dependency>
  16. <groupId>org.springframework</groupId>
  17. <artifactId>spring-test</artifactId>
  18. <version>5.1.5.RELEASE</version>
  19. </dependency>
  20. <dependency>
  21. <groupId>junit</groupId>
  22. <artifactId>junit</artifactId>
  23. <version>4.12</version>
  24. </dependency>
  25. </dependencies>

接口与实现类等

  1. public interface AccountService {
  2. public void transfer();
  3. }
  4. @Service
  5. public class AccountServiceImpl implements AccountService {
  6. @Override
  7. public void transfer() {
  8. System.out.println("转账业务...");
  9. }
  10. }
  11. @Component
  12. @Aspect
  13. public class MyAdvice {
  14. @Before("execution(* com.lagou..*.*(..))")
  15. public void before() {
  16. System.out.println("前置通知...");
  17. }
  18. }
  1. 核心配置文件
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:aop="http://www.springframework.org/schema/aop"
  5. xmlns:context="http://www.springframework.org/schema/context"
  6. xsi:schemaLocation="http://www.springframework.org/schema/beans
  7. http://www.springframework.org/schema/beans/spring-beans.xsd
  8. http://www.springframework.org/schema/aop
  9. http://www.springframework.org/schema/aop/spring-aop.xsd
  10. http://www.springframework.org/schema/context
  11. http://www.springframework.org/schema/context/spring-context.xsd">
  12. <context:component-scan base-package="com.ning"/>
  13. <!-- 强制使用动态代理 proxy-target-class="true"-->
  14. <aop:aspectj-autoproxy proxy-target-class="true"/>
  15. </beans>

测试代码

  1. @RunWith(SpringJUnit4ClassRunner.class)
  2. @ContextConfiguration("classpath:applicationContext.xml")
  3. class AccountServiceTest {
  4. @Autowired
  5. private AccountService accountService;
  6. @Test
  7. public void testTransfer() throws Exception {
  8. accountService.transfer();
  9. }
  10. }

切点表达式

  1. @Component
  2. @Aspect
  3. public class MyAdvice {
  4. @Pointcut("execution(* com.ning.service.impl.AccountServiceImpl.*(..))")
  5. public void myPoint() {
  6. }
  7. @Before("MyAdvice.myPoint()")
  8. public void before() {
  9. System.out.println("前置通知执行了");
  10. }
  11. @AfterReturning("MyAdvice.myPoint()")
  12. public void afterReturning() {
  13. System.out.println("后置通知执行了");
  14. }
  15. }

通知类型

image.png

纯注解配置

  1. @Configuration
  2. @ComponentScan("com.lagou")
  3. @EnableAspectJAutoProxy //替代 <aop:aspectj-autoproxy />
  4. public class SpringConfig {
  5. }