写点什么

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

  • 2022 年 5 月 15 日
  • 本文字数:3331 字

    阅读完需:约 11 分钟

为了快速复现这个问题,咱使用 SpringBoot + shiro-spring-boot-web-starter 构建。( SpringBoot 版本只要在 2.x 就可以,本文测试功能选用 2.2.8 )

[](()pom

关键的依赖有下面 4 个:


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 ,可以发现数据已经成功初始化了:



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



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


[](()问题排查




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

[](()检查两个 Service 对象

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



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

[](()检查 Service 的创建时机

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


@Service


public class DemoService {


public DemoService() {


System.out.println("DemoService constructor run ......");


}


复制代码


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



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



哦,合着我这个 DemoService 在 refresh 方法的后置处理器注册步骤就已经创建好了啊!小伙伴们要知道,SpringFramework 中 ApplicationContext 的初始化流程,一定是先把后置处理器都注册好了,再创建单实例 Bean 。但是这里很 《一线大厂 Java 面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 明显是后置处理器还没完全处理完,就引发单实例 Bean 的创建了!


[](()问题解决




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


spring boot shiro 事务无效


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


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


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


[](()原理扩展




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

[](()Shiro 提早创建 Realm 的原因

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

用户头像

还未添加个人签名 2022.04.13 加入

还未添加个人简介

评论

发布
暂无评论
SpringFramework中的注解声明式事务怎么被Shiro搞失效了_Java_爱好编程进阶_InfoQ写作社区