项⽬注解全部爆红_SpringFramework中的注解声明式事务,
怎么被Shiro搞失效了?
推荐学习
刷透近200道数据结构与算法,成功加冕“题王”,挤进梦中的字节
最全Java⾯试清单:基础-分布式-Spring-数据库-并发-缓存-⾼级
原问题
有位⼩伙伴的项⽬中,有⼀部分 Service 的注解事务⼀直不起作⽤,但也只是⼀部分起作⽤,也有⼀部分是好的。⽽且更奇怪的是,如果他把⼀个事务不起作⽤的 ServiceImpl 代码完整的抄⼀遍到新的复制类⾥头,那个类居然是有事务的!
初步分析
咱回想⼀下,按常理来讲,SpringFramework 中的事务不⽣效,⼤概有这么⼏种情况:
@Transactional 注解标注在⾮ public ⽅法上
@Transactional 注解标注在接⼝上,但实现类使⽤ Cglib 代理
@Transactional 注解标注抛出 Exception ,默认不捕捉
service ⽅法中⾃⾏ try-catch 了异常但没有再抛出 RuntimeException
原⽣ SSM 开发中,⽗⼦容器⼀起包扫描,会导致⼦容器先扫描到 service 并注册到⼦容器中但不加载事务,之后虽然⽗容器也扫描到service 但因为⼦容器中的 controller 已经注⼊了没有事务代理的 service ,会导致事务失效
声明式事务的配置必须由⽗ IOC 容器加载,SpringWebMvc 的⼦ IOC 容器加载不⽣效
除此之外,如果使⽤的关系型数据库是 MySQL ,还要关注是否为 InnoDB 引擎( MyISAM 不⽀持事务)。
结果⼩伙伴⼀通分析,发现这上⾯罗列的情况都没有出现,排查难度进⼀步加⼤。
新的关注点
隔了⼤概半天吧,那位⼩伙伴突然发现了⼀点问题:他们的项⽬使⽤了 Shiro 作为权限校验框架,⽽且那些事务失效的 Service 刚好就是
被 Shiro 中⾃定义 Realm 依赖的 Service !有了这个线索,下⾯排查起来就容易⼀些了。
问题复现
咱也⾃⼰搞⼀套,看看是不是像他说的那样吧!
⼯程搭建
为了快速复现这个问题,咱使⽤ SpringBoot + shiro-spring-boot-web-starter 构建。( SpringBoot 版本只要在 2.x 就可以,本⽂测试功能选⽤ 2.2.8 )
pom
关键的依赖有下⾯ 4 个:
org.springframework.boot    spring-boot-starter-weborg.springframework.boot    spring-boot-starter-jdbcorg.apache.shiro    shiro-spring-boot-web-starter
Realm
Shiro 的⾃定义策略核⼼就是 Realm ,咱也不整那些花⾥胡哨的,直接糊弄下算了。
public class CustomRealm extends AuthorizingRealm {        @Override    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token
配置类
只声明 Realm 还不够,需要定义⼏个 Bean 来补充必需的组件才⾏。
@Configurationpublic class ShiroConfiguration {        // ⾃定义Realm注册    @Bean    public CustomRealm authorizer() {        return new CustomRealm();    }        /数据库配置
快速搭建临时测试的、结构很简单的数据库,选择 h2 内存数据库更为合适。
application.properties 中配置 h2 的数据源及初始化数据库的 SQL :
spring.datasource.driver-class-name=org.h2.Driverspring.datasource.url=jdbc:h2:mem:shiro-testspring.datasource.username=saspring.datasource.passw 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) value
编写测试代码
下⾯就可以按照三层架构来写⼀些很简单的测试代码了。
DemoDao
这⾥咱就不整合 MyBatis / Hibernate 了,直接使⽤原⽣的 JdbcTemplate 就可以:
@Repositorypublic class DemoDao {        @Autowired    JdbcTemplate jdbcTemplate;        public List> findAll() {        return jdbcTemplate.query("select * fr
DemoService + DemoService2
声明⼀个会触发抛出运⾏时异常的⽅法,并标注 @Transactional 注解:
@Servicepublic class DemoService {        @Autowired    DemoDao demoDao;        @Transactional(rollbackFor = Exception.class)    public void doTransac
DemoService2 同样的代码,仅仅是类名不同,代码不再贴出。
DemoController
Controller ⾥⾯同时依赖这两个 Service :
@RestControllerpublic class DemoController {        @Autowired    DemoService demoService;        @Autowired    DemoService2 demoService2;        @Ge
Realm依赖Service
最后,让⾃定义的 Realm 依赖咱刚写的 DemoService :
public class CustomRealm extends AuthorizingRealm {        @Autowired    DemoService demoService;    // ......
运⾏测试
执⾏ SELECT * FROM SYS_DEPARTMENT ,可以发现数据已经成功初始化了:
下⾯测试事务,在浏览器输⼊ localhost:8080/doTransaction ,浏览器⾃然会报除零异常,但刷新数据库,会发现数据库真的多了⼀条insert 过去的数据!请求 /doTransaction2 则不会插⼊新的数据。
到这⾥,问题就真的发⽣了,下⾯要想办法解决这个问题才⾏。
问题排查
既然两个 Service 在代码上完全⼀致,只是⼀个被 Realm 依赖了,⼀个没有依赖⽽已,那总不能是这两个 Service 本来就不⼀样吧!
检查两个Service对象
将断点打在 /doTransaction 对应的⽅法上,Debug 重新启动⼯程,待断点落下后,发现被 Realm 依
赖的 DemoService 不是代理对象,⽽没有被 Realm 依赖的 DemoService2 经过事务的增强,成为了⼀个代理对象:
所以由此就可以看到问题所在了吧!上⾯的那个 DemoService 都没经过事务代理,凭什么能⽀持事务呢
检查Service的创建时机
既然两个 Service 都不是⼀个样的,那咱就看看这俩对象都啥时候创建的吧!给 DemoService 上显式的添加上⽆参构造⽅法,⽅便过会Debug :
@Servicepublic class DemoService {        public DemoService() {        System.out.println("DemoService constructor run ......");    }
重新以 Debug 运⾏,等断点打在构造⽅法中,观察⽅法调⽤栈:
看上去还⽐较正常吧,但如果往下拉到底,这问题就太严重了:
哦,合着我这个 DemoService 在 refresh ⽅法的后置处理器注册步骤就已经创建好了啊!⼩伙伴们要知道,SpringFramework 中ApplicationContext 的初始化流程,⼀定是先把后置处理器都注册好了,再创建单实例 Bean 。但是这⾥很明显是后置处理器还没完全处
理完,就引发单实例 Bean 的创建了!
问题解决
总的来看,解决⽅案的核⼼在于:如何让 Realm 创建时不⽴即依赖创建 DemoService ,所以就有两种解决⽅案了:要么延迟初始化DemoService ,要么把⾃定义的 Realm 和 SecurityManager 放在⼀个额外的空间,利⽤机制创建它们。具体的实现可以参照上
⾯⽂章的写法,这⾥就不赘述了。
原理扩展
解决问题之后,如果能从这⾥⾯了解到⼀点更深⼊的原理知识,想必那是最好不过了。下⾯就这个问题出现的原因,以及上⾯ @Lazy ⽅案
的原理,咱都深⼊解析⼀下。
Shiro提早创建Realm的原因
既然上⾯看到了⽅法调⽤栈中,DemoService 被⾃定义 Realm 依赖后在 ApplicationContext 的 refresh 阶段的registerBeanPostProcessors 中就已经被触发创建,可它为什么⾮要搞这⼀出呢?⾃定义 Realm 放到 finishBeanFactoryInitialization 中统⼀创建不好吗?下⾯咱通过 Debug 研究问题的成因。
Debug运⾏
spring ioc注解DemoService 中的断点不要去掉,重新 Debug 让断点停在那⾥,翻到最底下的调⽤栈,查看那个正在创建的 BeanPostProcessor ,发现它的名称是 shiroEventBusAwareBeanPostProcessor :
Shiro的后置处理器创建
翻开创建 shiroEventBusAwareBeanPostProcessor 的位置,在 ShiroBeanAutoConfiguration 中,它
⼜依赖了⼀个 EventBus :
@Bean@ConditionalOnMissingBean@Overridepublic ShiroEventBusBeanPostProcessor shiroEventBusAwareBeanPostProcessor() {    return super.shiroE 顺着⽅法调⽤栈往上爬,到下⼀个 doCreateBean ,发现确实有创建 eventBus 的部分:
再往上爬,发现这上⾯有⼀个 wrapIfNecessary ⽅法的调⽤,很明显这是要搞 AOP 增强了啊:
AOP 的增强需要先获取到增强器,继续往上爬⽅法调⽤,在 findAdvisorBeans ⽅法中到了两个适配的增强器:
上⾯的是 Shiro 的授权相关的增强器,下⾯是 SpringFramework 中的事务控制增强器。
触发AOP增强器的创建
根据迭代顺序,先取出下⾯的事务控制增强器 TransactionAdvisor ,由于获取到增强器的 Bean 也是需要⾛统⼀的 getBean ⽅法,所以
在⽅法调⽤栈中,咱⼜⼀次看到了 getBean ⽅法,继续往下创建。
由于在 SpringFramework 中,使⽤ @Configuration + @Bean 声明的 Bean ,都是要先把配置类初始化好,才能创建 Bean 。所以继
续往上爬调⽤栈时,会发现它并没有接着创建 Shiro 的增强器 authorizationAttributeSourceAdvisor ,⽽是先初始化了声明有TransactionAttributeSourceAdvisor 的配置类 ProxyTransactionManagementConfiguration :
再往上⾛,发现⼜出现了⼀次 wrapIfNecessary ⽅法,说明配置类也会被 AOP 增强。那就重复⼀遍上⾯的步骤,继续遇到这两个增强
器。
这个时候可能有⼩伙伴产⽣疑问了:这次创建就是因为上⾯的 TransactionAttributeSourceAdvisor 创建才跟过来的,这次还要再创建,
这是闹哪出呢?放⼼,咱都想到这个问题了,⼈家写 SpringFramework 的⼤佬们能想不到吗?所以在创建之前,它加了⼀个判断:
if (this.beanFactory.isCurrentlyInCreation(name)) {        // 如果当前bean正在创建,则跳过        if (logger.isTraceEnabled()) {            ace("Skipping curre
这⾥巧妙的利⽤了 singletonsCurrentlyInCreation 这个集合,判断了当前增强器是否在创建,这样就不会出现重复创建⽆限死循环的问
题了。
singletonsCurrentlyInCreation 的存放是在 getSingleton ⽅法调⽤时就已经放进去了,所以能很稳妥的记录下当前正在创建的所
有 Bean ,防⽌死循环重复创建。
Shiro增强器的创建
上⾯的事务控制增强器跳过去了,那就可以创建 Shiro 的增强器了:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。