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 。但是这里很明显是后置处理器还没完全处理完,就引发单实例 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 :
Shiro的后置处理器创建
翻开创建 shiroEventBusAwareBeanPostProcessor 的位置,在 ShiroBeanAutoConfiguration 中,它又依赖了一个 EventBus :
@Bean
@ConditionalOnMissingBean
@Override
public ShiroEventBusBeanPostProcessor shiroEventBusAwareBeanPostProcessor() {
return super.shiroEventBusAwareBeanPostProcessor();
}
protected ShiroEventBusBeanPostProcessor shiroEventBusAwareBeanPostProcessor() {
return new ShiroEventBusBeanPostProcessor(eventBus());
}
复制代码
顺着方法调用栈往上爬,找到下一个 doCreateBean ,发现确实有创建 eventBus 的部分:
再往上爬,发现这上面有一个 wrapIfNecessary 方法的调用,很明显这是要搞 AOP 增强了啊:
AOP 的增强需要先获取到增强器,继续往上爬方法调用,在 findAdvisorBeans 方法中找到了两个适配的增强器:
上面的是 Shiro 的授权相关的增强器,下面是 SpringFramework 中的事务控制增强器。
触发AOP增强器的创建
根据迭代顺序,先取出下面的事务控制增强器 TransactionAdvisor ,由于获取到增强器的 Bean 也是需要走统一的 getBean 方法,所以在方法调用栈中,咱又一次看到了 getBean 方法,继续往下创建。
由于在 SpringFramework 中,使用 @Configuration + @Bean 声明的 Bean ,都是要先把配置类初始化好,才能创建 Bean 。所以继续往上爬调用栈时,会发现它并没有接着创建 Shiro 的增强器 authorizationAttributeSourceAdvisor ,而是先初始化了声明有 TransactionAttributeSourceAdvisor 的配置类 ProxyTransactionManagementConfiguration :
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://i-blog.csdnimg.cn/blog_migrate/25edba60866bed7c044814a4ec3cebd7.jpeg)
分享
1、算法大厂——字节跳动面试题
2、2000页互联网Java面试题大全
3、高阶必备,算法学习
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
链图片转存中…(img-KNcndKJD-1713330162563)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://i-blog.csdnimg.cn/blog_migrate/25edba60866bed7c044814a4ec3cebd7.jpeg)
分享
1、算法大厂——字节跳动面试题
[外链图片转存中…(img-7EWFPAEL-1713330162563)]
2、2000页互联网Java面试题大全
[外链图片转存中…(img-aF3dIFld-1713330162563)]
3、高阶必备,算法学习
[外链图片转存中…(img-3IH6xjdv-1713330162564)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!