博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
一个案例教你理解Spring面向切面编程(Spring Aop)
阅读量:1888 次
发布时间:2019-04-26

本文共 14680 字,大约阅读时间需要 48 分钟。

文章目录

Spring Aop 案例引入

Aspect oriented programming 面向切面编程

AOP (Aspect Orient Programming),直译过来就是 面向切面编程。AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。

在这里插入图片描述

从该图可以很形象地看出,所谓切面,相当于应用对象间的横切点,我们可以将其单独抽象为单独的模块。

前期案例:账户转账

账户类、dao、service具体代码和前面类似

service类的转账方法

/**     * 转账     * @param account1 转出账户     * @param account2 转入账户     * @param money 转账金额     * @throws SQLException     */public void transfer(Account account1,Account account2,double money) throws SQLException {
account1.setMoney(account1.getMoney()-money); account2.setMoney(account2.getMoney()+money); update(account1); update(account2);}

模拟转账异常

/**     * 转账     * @param account1 转出账户     * @param account2 转入账户     * @param money 转账金额     * @throws SQLException     */public void transfer(Account account1,Account account2,double money) throws SQLException {
account1.setMoney(account1.getMoney()-money); account2.setMoney(account2.getMoney()+money); update(account1); int i=1/0; //这里抛出异常,下面代码不再执行,足见职务的重要性 update(account2);}

存在问题

  1. 没有开启事务
  2. 就算开启事务,由于我们每次获取的数据库连接都不同,转账方法里面可能会用到好几个连接,每条语句执行完之后提交各自的事务,无法完成整体的转账事务

需要使用ThreadLocal对象把Connection和当前线程绑定,从而使一个线程中只有一个能控制事务的Connection对象

事务都应该在业务层

案例改进

  • 使用ThreadLocal对象绑定线程和Connection
  • 使用事务管理类管理事务

在这里插入图片描述

ConnectionUtils.java

@Component("connectionUtils")public class ConnectionUtils {
private ThreadLocal
threadLocal=new ThreadLocal<>(); @Resource(name = "dataSource") private DataSource dataSource; public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource; } /** * 获取当前线程上的连接 * @return * @throws SQLException */ public Connection getThreadConnection() throws SQLException {
Connection connection =threadLocal.get(); if (connection==null){
connection=dataSource.getConnection(); threadLocal.set(connection); } return connection; } /** * 把当前线程和Connection解绑 */ public void remove(){
threadLocal.remove(); }}

TransactionManager.java

/** * @ClassName TransactionManager 事务管理工具类,开启,提交,回滚,关闭连接 */@Component("transactionManager")public class TransactionManager {
@Resource(name = "connectionUtils") private ConnectionUtils connectionUtils; /** * 开启事务 */ public void beginTransaction(){
try {
connectionUtils.getThreadConnection().setAutoCommit(false); } catch (SQLException e) {
e.printStackTrace(); } } /** * 提交事务 */ public void commit(){
try {
connectionUtils.getThreadConnection().commit(); } catch (SQLException e) {
e.printStackTrace(); } } /** * 回滚事务 */ public void rollback(){
try {
connectionUtils.getThreadConnection().rollback(); } catch (SQLException e) {
e.printStackTrace(); } } /** * 释放连接 */ public void release(){
try {
connectionUtils.getThreadConnection().close(); connectionUtils.remove(); } catch (SQLException e) {
e.printStackTrace(); } }}

AccountDao.java

Dao类应注意:

不能再直接从DataSource中获得连接,即QueryRunner对象中不能注入数据源

而应该使用从ThreadLocal对象中获取的Connection

执行sql语句时,在参数里面加上Connection对象

runner.query(connectionUtils.getThreadConnection(),sql, new BeanListHandler<Account>(Account.class));

package com.console.dao;import com.console.domain.Account;import com.console.utils.ConnectionUtils;import org.apache.commons.dbutils.QueryRunner;import org.apache.commons.dbutils.handlers.BeanHandler;import org.apache.commons.dbutils.handlers.BeanListHandler;import org.springframework.stereotype.Repository;import javax.annotation.Resource;import java.sql.SQLException;import java.util.List;/** * @author caozj * @version 1.0 * @className AccountDao * @date 2020/7/29 15:10 */@Repository("accountDao")public class AccountDao {
@Resource(name = "queryRunner") private QueryRunner runner; @Resource(name = "connectionUtils") private ConnectionUtils connectionUtils; /** * 获取所有账户对象 * @return * @throws Exception */ public List
allAccount() throws Exception {
String sql = "select * from account"; return runner.query(connectionUtils.getThreadConnection(),sql, new BeanListHandler
(Account.class)); } ......}

AccountService.java

在Service中使用事务管理,流程如下:

try{
//开启事务 transactionManager.beginTransaction(); //执行操作 accountDao.deleteById(id); //提交事务 transactionManager.commit();}catch (Exception e){
e.printStackTrace(); //回滚事务 transactionManager.rollback();}finally {
//释放连接 transactionManager.release();}
/** * @className AccountService 账户服务类 */@Service("accountService")public class AccountService {
@Resource(name = "accountDao") private AccountDao accountDao; @Resource(name = "transactionManager") private TransactionManager transactionManager; /** * 转账 * @param account1 转出账户 * @param account2 转入账户 * @param money 转账金额 * @throws SQLException */ public void transfer(Account account1,Account account2,double money) throws SQLException {
try{
//开启事务 transactionManager.beginTransaction(); //执行操作 account1.setMoney(account1.getMoney()-money); account2.setMoney(account2.getMoney()+money); accountDao.update(account1); int i=1/0; accountDao.update(account2); //提交事务 transactionManager.commit(); }catch (Exception e){
e.printStackTrace(); //回滚事务 transactionManager.rollback(); throw e; }finally {
//释放连接 transactionManager.release(); } }}

至此,使用事务改造过的案例已经完成。

开启事务之后案例中存在的问题

改进后的案例有了事务支持,但是每个方法都对事务进行开启提交或者回滚,整个Service类显得臃肿

改进:使用动态代理,将事务的支持在代理类中实现

基于接口的动态代理实现

被代理类必须实现特定接口

使用Proxy.newProxyInstance()创建代理对象

使用动态代理对案例改进

在这里插入图片描述

AccountServiceWithouttransaction类为使用没有添加事务之前的AccountService版本

BeanFactory.java

/** * @ClassName BeanFactory 创建Service代理对象的工厂 */@Component("beanFactory")public class BeanFactory {
@Resource(name = "accountService") private IAccountService accountService; @Resource(name = "transactionManager") TransactionManager transactionManager; /** * 获取accountService的代理对象 * @return */ public IAccountService getAccountService(){
IAccountService obj=(IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() {
/** * 添加事务的支持 * @param o * @param method * @param objects * @return * @throws Throwable */ @Override public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
Object returnValue=null; try{
transactionManager.beginTransaction(); //这里实际执行的是原Service类中的方法 returnValue=method.invoke(accountService,objects); transactionManager.commit(); return returnValue; }catch (Exception e){
transactionManager.rollback(); throw e; }finally {
transactionManager.release(); } } }); return obj; }}

使用动态代理类之后,以后使用AccountService的时候使用代理对象即可实现事务支持。

再次理解Aop概念

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

基于xml配置的Aop实现案例

案例介绍:在业务类的方法执行之前切入日志记录的操作

pom.xml中的依赖配置

org.springframework
spring-context
5.2.3.RELEASE
org.aspectj
aspectjweaver
1.8.7
javax.annotation
javax.annotation-api
1.3.1
junit
junit
4.12
test
org.springframework
spring-test
5.2.1.RELEASE

IAccountService业务接口

/** * 这里一定要有接口,因为SprigAop使用基于接口的动态代理实现Aop */public interface IAccountService {
/** * 模拟更新 */ public void update(); /** * 模拟获取用户数 * @return */ public int getCount(); /** * 模拟使用id查找 * @param i */ public void queryId(int i);}

AccountService模拟业务实现类

/** * @ClassName AccountService 模拟服务 */@Service("accountService")public class AccountService implements IAccountService{
@Override public void update(){
System.out.println("update account"); } @Override public int getCount(){
System.out.println("getCount()"); int i=(int)Math.floor(Math.random()); return i; } @Override public void queryId(int i){
System.out.println("query id "+i); }}

Logger类,模拟动态代理中代理类拓展的业务

package com.console.utils;/** * @ClassName logger 记录日志的工具类 */@Component("logger")public class Logger {
public void printLog(){
System.out.println("Logger
:打印日志 "+ new Date().toString()); } public void afterReturning(){
System.out.println("afterReturning "+new Date().toString()); } public void afterThrow(){
System.out.println("afterThrow "+new Date().toString()); } public void after(){
System.out.println("after "+new Date().toString()); }}

配置文件Aop.xml

测试类

@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations = "classpath:springAop.xml")public class AccountServiceTest {
@Resource(name = "accountService") private IAccountService accountService; //这里注入的accountService对象一定要用接口类型来声明,不能用实现类来声明,负责会出错误 @Before public void setUp() throws Exception {
} @Test public void update() {
accountService.update(); } @Test public void getCount() {
accountService.getCount(); } @Test public void queryId() {
accountService.queryId(10); }}

XML配置Aop详解

pointcut可以使用通配符指定

  • 访问修饰符可以不用写
  • public void com.console.service.AccountService.update()void com.console.service.AccountService.update()相同
  • *表示所有
    • * com.console.service.AccountService.update()
    • * *.*.*.AccountService.update() 包名层级要写完
    • 使用*..表示当前包及其子包
    • * *..AccountService.update()
    • * *..*.update() 所有包下所有类的update()无参数方法
    • * *..*.*(int) 所有包下所有类的参数为int的方法
    • * *..*.*(..) 所有包下所有类的所有方法的所有参数列表的方法(包括无参数方法)

pointcut-ref

如果pointcut需要多次使用,可以使用 aop:pointcut 标签声明,再使用pointcut-ref引入

注意:aop:pointcut 可以写在 aop:aspect标签内部,也可以写在外部,如果写在外部,必须写在aop:aspect标签前面

通知的类型

  • before 前置通知 :切入点之前执行
  • afterReturning 后置通知:返回结果后执行
  • afterThrowing 异常通知:抛出异常通知
  • after 最终通知: 所有完成后通知,类似于try-catch-finally语句块中的代码

环绕通知

环绕通知是一种方式,和前面四种通知类型不同,环绕通知可以同时配置所有四种类型的通知

切面类Logger.java 中对应的环绕方法,和动态代理中第三个参数类似

public  Object around(ProceedingJoinPoint proceedingJoinPoint){
Object rtValue=null; try {
System.out.println("前置通知执行 "+new Date()); //下面这句显式调用切入点的业务方法 rtValue=proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs()); System.out.println("后置通知执行 "+new Date()); return rtValue; } catch (Throwable throwable) {
throwable.printStackTrace(); System.out.println("异常通知执行 "+new Date()); throw new RuntimeException(throwable); }finally {
System.out.println("最终通知执行 "+new Date()); }}

基于注解的Aop配置

Logger.java

package com.console.utils;/** * @ClassName logger 记录日志的工具类 */@Component("logger")@Aspect //当前类是切面类public class Logger {
//定义切入点 @Pointcut( "execution(* com.console.service.AccountService.*(..))") private void getPointcut(){
} @Before("getPointcut()") public void printLog(){
System.out.println("Logger
:打印日志 "+ new Date().toString()); } @AfterReturning("getPointcut()") public void afterReturning(){
System.out.println("afterReturning "+new Date().toString()); } @AfterThrowing("getPointcut()") public void afterThrowing(){
System.out.println("afterThrow "+new Date().toString()); } @After("getPointcut()") public void after(){
System.out.println("after "+new Date().toString()); }// @Around("getPointcut()") public Object around(ProceedingJoinPoint proceedingJoinPoint){
Object rtValue=null; try {
System.out.println("前置通知执行 "+new Date()); rtValue=proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs()); System.out.println("后置通知执行 "+new Date()); return rtValue; } catch (Throwable throwable) {
throwable.printStackTrace(); System.out.println("异常通知执行 "+new Date()); throw new RuntimeException(throwable); }finally {
System.out.println("最终通知执行 "+new Date()); } }}

Spring配置文件

测试类

package com.console.service;@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations = "classpath:springAopAnnotation.xml")public class AccountServiceTest {
@Resource(name = "accountService") private IAccountService accountService; @Before public void setUp() throws Exception {
} @Test public void update() {
accountService.update(); } @Test public void getCount() {
accountService.getCount(); } @Test public void queryId() {
accountService.queryId(10); }}

注意

  • 使用注解的切面配置的时候,Spring调用顺序和配置文件配置的Aop有出入

    • 先调用after,即最终通知,再调用异常或者后置通知
  • 使用环绕通知可以解决这个问题

  • 不适用xml的注解

    配置文件中的<aop:aspectj-autoproxy></aop:aspectj-autoproxy>可以使用@EnableAspectJAutoProxy注解代替

@Aspect //当前类是切面类@EnableAspectJAutoProxypublic class Logger {
}

转载地址:http://jezdf.baihongyu.com/

你可能感兴趣的文章
mysql数据库操作基础
查看>>
Mariadb基础管理
查看>>
kolla-ansible部署openstack+ceph高可用集群queens版本--- 部署说明
查看>>
kolla-ansible部署openstack+ceph高可用集群queens版本--- 环境准备及初始化
查看>>
kolla-ansible部署openstack+ceph高可用集群queens版本---docker私有镜像仓库配置
查看>>
mysql 中com.mysql.jdbc.PacketTooBigException 解决办法
查看>>
awk 的内置变量 NF、NR、FNR、FS、OFS、RS、ORS
查看>>
CentOS系统内核升级攻略
查看>>
linux系统时区修改(Debian的主机和docker)
查看>>
docker-compose 安装
查看>>
crontab 定时任务
查看>>
查看docker veth pair与宿主机上网卡的对应关系
查看>>
使用 GitLab CI 进行持续集成的一些踩坑
查看>>
企业云盘给贸易业带来新的效益
查看>>
Linux入门常用命令
查看>>
Spring整理
查看>>
SpringMvc加强
查看>>
初识Vue全家桶 Nuxt.js(一)
查看>>
基本路由及动态路由(二)
查看>>
视图:默认模板+默认布局(自定义布局)+nuxt.js页面(三)
查看>>