SpringFramework 中的注解声明式事务怎么被 Shiro 搞失效了
为了快速复现这个问题,咱使用 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 研究问题的成因。
评论