SpringFramework中的注解声明式事务怎么被Shiro搞失效了

org.springframework.boot

spring-boot-starter-web

org.springframework.boot

spring-boot-starter-jdbc

org.apache.shiro

shiro-spring-boot-web-starter

1.5.3

com.h2database

h2

1.4.199

复制代码

Realm

Shiro 的自定义策略核心就是 Realm ,咱也不整那些花里胡哨的,直接糊弄下算了。

public class CustomRealm extends AuthorizingRealm {

@Override

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

if (token.getPrincipal() == null) {

return null;

}

String name = token.getPrincipal().toString();

// 请求数据库查询是否存在用户,这里省略

return new SimpleAuthenticationInfo(name, “123456”, getName());

}

@Override

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

// 请求数据库/缓存加载用户的权限,这里暂时使用一组假数据

authorizationInfo.addStringPermissions(Arrays.asList(“aa”, “bb”, “cc”));

return authorizationInfo;

}

}

复制代码

配置类

只声明 Realm 还不够,需要定义几个 Bean 来补充必需的组件才行。

@Configuration

public class ShiroConfiguration {

// 自定义Realm注册

@Bean

public CustomRealm authorizer() {

return new CustomRealm();

}

// 动态代理创建器(上面没有导入AOP)

@Bean

public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {

DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();

advisorAutoProxyCreator.setProxyTargetClass(true);

return advisorAutoProxyCreator;

}

// 过滤器定义,此处选择全部放行,方便调试

@Bean

public ShiroFilterChainDefinition filterChainDefinition() {

DefaultShiroFilterChainDefinition filterChainDefinition = new DefaultShiroFilterChainDefinition();

filterChainDefinition.addPathDefinition(“/**”, “anon”);

return filterChainDefinition;

}

}

复制代码

数据库配置

快速搭建临时测试的、结构很简单的数据库,选择 h2 内存数据库更为合适。

application.properties 中配置 h2 的数据源及初始化数据库的 SQL :

spring.datasource.driver-class-name=org.h2.Driver

spring.datasource.url=jdbc:h2:mem:shiro-test

spring.datasource.username=sa

spring.datasource.password=sa

spring.datasource.platform=h2

spring.datasource.schema=classpath:sql/schema.sql

spring.datasource.data=classpath:sql/data.sql

spring.h2.console.settings.web-allow-others=true

spring.h2.console.path=/h2

spring.h2.console.enabled=true

复制代码

resources 目录下创建 sql 文件夹,并创建两个 .sql 文件,分别声明数据库表的结构和数据:

create table if not exists sys_department (

id varchar(32) not null primary key,

name varchar(32) not null

);


insert into sys_department (id, name) values (‘idaaa’, ‘testaaa’);

insert into sys_department (id, name) values (‘idbbb’, ‘testbbb’);

insert into sys_department (id, name) values (‘idccc’, ‘testccc’);

insert into sys_department (id, name) values (‘idddd’, ‘testddd’);

复制代码

编写测试代码

下面就可以按照三层架构来写一些很简单的测试代码了。

DemoDao

这里咱就不整合 MyBatis / Hibernate 了,直接使用原生的 JdbcTemplate 就可以:

@Repository

public class DemoDao {

@Autowired

JdbcTemplate jdbcTemplate;

public List

DemoService + DemoService2

声明一个会触发抛出运行时异常的方法,并标注 @Transactional 注解:

@Service

public class DemoService {

@Autowired

DemoDao demoDao;

@Transactional(rollbackFor = Exception.class)

public void doTransaction() {

demoDao.save(“aaaaaaaa”);

int i = 1 / 0;

demoDao.update(“18”, “ccc”);

}

}

复制代码

DemoService2 同样的代码,仅仅是类名不同,代码不再贴出。

DemoController

Controller 里面同时依赖这两个 Service :

@RestController

public class DemoController {

@Autowired

DemoService demoService;

@Autowired

DemoService2 demoService2;

@GetMapping(“/doTransaction”)

public String doTransaction() {

demoService.doTransaction();

return “doTransaction”;

}

@GetMapping(“/doTransaction2”)

public String doTransaction2() {

demoService2.doTransaction();

return “doTransaction2”;

}

}

复制代码

Realm依赖Service

最后,让自定义的 Realm 依赖咱刚写的 DemoService :

public class CustomRealm extends AuthorizingRealm {

@Autowired

DemoService demoService;

// …

复制代码

运行测试

运行 SpringBoot 的主启动类,在浏览器输入 http://localhost:8080/h2 输入刚才在 properties 文件中声明的配置,即可打开 h2 数据库的管理台。

执行 SELECT * FROM SYS_DEPARTMENT ,可以发现数据已经成功初始化了:

image

下面测试事务,在浏览器输入 localhost:8080/doTransaction ,浏览器自然会报除零异常,但刷新数据库,会发现数据库真的多了一条 insert 过去的数据!请求 /doTransaction2 则不会插入新的数据。

image

到这里,问题就真的发生了,下面要想办法解决这个问题才行。

问题排查


既然两个 Service 在代码上完全一致,只是一个被 Realm 依赖了,一个没有依赖而已,那总不能是这两个 Service 本来就不一样吧!

检查两个Service对象

将断点打在 /doTransaction 对应的方法上,Debug 重新启动工程,待断点落下后,发现被 Realm 依赖的 DemoService 不是代理对象,而没有被 Realm 依赖的 DemoService2 经过事务的增强,成为了一个代理对象:

image

所以由此就可以看到问题所在了吧!上面的那个 DemoService 都没经过事务代理,凭什么能支持事务呢???

检查Service的创建时机

既然两个 Service 都不是一个样的,那咱就看看这俩对象都啥时候创建的吧!给 DemoService 上显式的添加上无参构造方法,方便过会 Debug :

@Service

public class DemoService {

public DemoService() {

System.out.println(“DemoService constructor run …”);

}

复制代码

重新以 Debug 运行,等断点打在构造方法中,观察方法调用栈:

image

看上去还比较正常吧,但如果往下拉到底,这问题就太严重了:

image

哦,合着我这个 DemoService 在 refresh 方法的后置处理器注册步骤就已经创建好了啊!小伙伴们要知道,SpringFramework 中 ApplicationContext 的初始化流程,一定是先把后置处理器都注册好了,再创建单实例 Bean 。但是这里很明显是后置处理器还没完全处理完,就引发单实例 Bean 的创建了!

问题解决


问题终于找明白了,咋解决呢?其实网上有的是现成的文章了:

spring boot shiro 事务无效

shiro导致springboot事务不起效解决办法

spring + shiro 配置中部分事务失效分析及解决方案

总的来看,解决方案的核心在于:如何让 Realm 创建时不立即依赖创建 DemoService ,所以就有两种解决方案了:要么延迟初始化 DemoService ,要么把自定义的 Realm 和 SecurityManager 放在一个额外的空间,利用监听器机制创建它们。具体的实现可以参照上面文章的写法,这里就不赘述了。

原理扩展


解决问题之后,如果能从这里面了解到一点更深入的原理知识,想必那是最好不过了。下面就这个问题出现的原因,以及上面 @Lazy 方案的原理,咱都深入解析一下。

Shiro提早创建Realm的原因

既然上面看到了方法调用栈中,DemoService 被自定义 Realm 依赖后在 ApplicationContext 的 refresh 阶段的 registerBeanPostProcessors 中就已经被触发创建,可它为什么非要搞这一出呢?自定义 Realm 放到 finishBeanFactoryInitialization 中统一创建不好吗?下面咱通过 Debug 研究问题的成因。

Debug运行

DemoService 中的断点不要去掉,重新 Debug 让断点停在那里,翻到最底下的调用栈,查看那个正在创建的 BeanPostProcessor ,发现它的名称是 shiroEventBusAwareBeanPostProcessor :

image

Shiro的后置处理器创建

翻开创建 shiroEventBusAwareBeanPostProcessor 的位置,在 ShiroBeanAutoConfiguration 中,它又依赖了一个 EventBus :

@Bean

@ConditionalOnMissingBean

@Override

public ShiroEventBusBeanPostProcessor shiroEventBusAwareBeanPostProcessor() {

return super.shiroEventBusAwareBeanPostProcessor();

}

protected ShiroEventBusBeanPostProcessor shiroEventBusAwareBeanPostProcessor() {

return new ShiroEventBusBeanPostProcessor(eventBus());

}

复制代码

顺着方法调用栈往上爬,找到下一个 doCreateBean ,发现确实有创建 eventBus 的部分:

image

再往上爬,发现这上面有一个 wrapIfNecessary 方法的调用,很明显这是要搞 AOP 增强了啊:

image

AOP 的增强需要先获取到增强器,继续往上爬方法调用,在 findAdvisorBeans 方法中找到了两个适配的增强器:

image

上面的是 Shiro 的授权相关的增强器,下面是 SpringFramework 中的事务控制增强器。

触发AOP增强器的创建

根据迭代顺序,先取出下面的事务控制增强器 TransactionAdvisor ,由于获取到增强器的 Bean 也是需要走统一的 getBean 方法,所以在方法调用栈中,咱又一次看到了 getBean 方法,继续往下创建。

由于在 SpringFramework 中,使用 @Configuration + @Bean 声明的 Bean ,都是要先把配置类初始化好,才能创建 Bean 。所以继续往上爬调用栈时,会发现它并没有接着创建 Shiro 的增强器 authorizationAttributeSourceAdvisor ,而是先初始化了声明有 TransactionAttributeSourceAdvisor 的配置类 ProxyTransactionManagementConfiguration :

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

分享

1、算法大厂——字节跳动面试题

2、2000页互联网Java面试题大全

3、高阶必备,算法学习

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
链图片转存中…(img-KNcndKJD-1713330162563)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

分享

1、算法大厂——字节跳动面试题

[外链图片转存中…(img-7EWFPAEL-1713330162563)]

2、2000页互联网Java面试题大全

[外链图片转存中…(img-aF3dIFld-1713330162563)]

3、高阶必备,算法学习

[外链图片转存中…(img-3IH6xjdv-1713330162564)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 25
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值