一)問題描述:
我在一個Spring的項目中使用shiro搭建權限控制框架。主要通過shiro-spring-boot-web-starter包快速集成Shiro。但是項目無法啟動,報沒有authorizer的bean的錯誤:
```
No bean named 'authorizer' available
```
我只好又在自己的Configuration中又配置了Authorizer,才能正常啟動。
@Configuration
public class ShiroConfig {
@Bean
public Authorizer authorizer(){
return new ModularRealmAuthorizer();
}
}
但是奇怪的明明athorizer是SecurityManager中一個重要的組件,為什么沒有在shiro starter的Configuration中被聲明為Bean?同樣的,Authenticator就沒問題?
二)明確shiro-spring-boot-web-starter是否有對應的聲明
我們在pom文件中聲明了shiro-spring-boot-web-starter。就從對應的jar包開始找起。
首先是META-INF中的spring.factories文件。我們知道spring-boot-starter都是通過在該文件中聲明Configuraion來達到集成自身配置的目的。
org.springframework.boot.autoconfigure.EnableAutoConfiguration = \
org.apache.shiro.spring.config.web.autoconfigure.ShiroWebAutoConfiguration,\
org.apache.shiro.spring.config.web.autoconfigure.ShiroWebFilterConfiguration
上述聲明了兩個Configration:ShiroWebAutoConfiguration和ShiroWebFilterConfiguration。
- ShiroWebFilterConfiguration
先從簡單的配置說起,ShiroWebFilterConfiguration是以添加Filter的方式來達到authentication的目的。這個和我們的問題無關,簡單帶過。 - ShiroWebAutoConfiguration
@Configuration
@AutoConfigureBefore(ShiroAutoConfiguration.class)
@ConditionalOnProperty(name = "shiro.web.enabled", matchIfMissing = true)
public class ShiroWebAutoConfiguration extends AbstractShiroWebConfiguration {
@Bean
@ConditionalOnMissingBean
@Override
protected AuthenticationStrategy authenticationStrategy() {
return super.authenticationStrategy();
}
@Bean
@ConditionalOnMissingBean
@Override
protected Authenticator authenticator() {
return super.authenticator();
}
@Bean
@ConditionalOnMissingBean
@Override
protected Authorizer authorizer() {
return super.authorizer();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SubjectDAO subjectDAO() {
return super.subjectDAO();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SessionStorageEvaluator sessionStorageEvaluator() {
return super.sessionStorageEvaluator();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SubjectFactory subjectFactory() {
return super.subjectFactory();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SessionFactory sessionFactory() {
return super.sessionFactory();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SessionDAO sessionDAO() {
return super.sessionDAO();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SessionManager sessionManager() {
return super.sessionManager();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SessionsSecurityManager securityManager(List<Realm> realms) {
return createSecurityManager();
}
@Bean
@ConditionalOnMissingBean(name = "sessionCookieTemplate")
@Override
protected Cookie sessionCookieTemplate() {
return super.sessionCookieTemplate();
}
@Bean
@ConditionalOnMissingBean
@Override
protected RememberMeManager rememberMeManager() {
return super.rememberMeManager();
}
@Bean
@ConditionalOnMissingBean(name = "rememberMeCookieTemplate")
@Override
protected Cookie rememberMeCookieTemplate() {
return super.rememberMeCookieTemplate();
}
@Bean
@ConditionalOnMissingBean
@Override
protected ShiroFilterChainDefinition shiroFilterChainDefinition() {
return super.shiroFilterChainDefinition();
}
}
這個配置類將Shiro需要的各組件都聲明成了bean,交給容器管理。具體的創建過程都在父類AbstractShiroWebConfiguration。可以看到確實是有聲明authorizer。但是為什么會找不到呢?是不是其他的配置文件聲明了類似的bean,產生了影響?
三)繼續查找其他配置
觀察shiro-spring-boot-web-starter的配置文件,可以看到它又引用了shiro-spring-boot-starter包。shrio-spring-boot-starter又是一個Spring boot starter包,同樣通過它的META-INF文件,可以知道加入了哪些Configuration:
org.springframework.boot.autoconfigure.EnableAutoConfiguration = \
org.apache.shiro.spring.boot.autoconfigure.ShiroBeanAutoConfiguration,\
org.apache.shiro.spring.boot.autoconfigure.ShiroAutoConfiguration,\
org.apache.shiro.spring.boot.autoconfigure.ShiroAnnotationProcessorAutoConfiguration
org.springframework.boot.diagnostics.FailureAnalyzer = \
org.apache.shiro.spring.boot.autoconfigure.ShiroNoRealmConfiguredFailureAnalyzer
最后一個文件是判斷項目中不存在Realm時,拋出異常。前面是我們需要關注的配置文件。
- ShiroAnnotationProcessorAutoConfiguration
該配置主要是通過AOP的方式實現authorization的功能。 - ShiroBeanAutoConfiguraion
主要是通過添加BeanPostProcessor,在Shiro相關的Bean初始化時,做一些額外的操作。 - ShiroAutoConfiguration
@Configuration
@SuppressWarnings("SpringFacetCodeInspection")
@ConditionalOnProperty(name = "shiro.enabled", matchIfMissing = true)
public class ShiroAutoConfiguration extends AbstractShiroConfiguration {
@Bean
@ConditionalOnMissingBean
@Override
protected AuthenticationStrategy authenticationStrategy() {
return super.authenticationStrategy();
}
@Bean
@ConditionalOnMissingBean
@Override
protected Authenticator authenticator() {
return super.authenticator();
}
@Bean
@ConditionalOnMissingBean
@Override
protected Authorizer authorizer() {
return super.authorizer();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SubjectDAO subjectDAO() {
return super.subjectDAO();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SessionStorageEvaluator sessionStorageEvaluator() {
return super.sessionStorageEvaluator();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SubjectFactory subjectFactory() {
return super.subjectFactory();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SessionFactory sessionFactory() {
return super.sessionFactory();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SessionDAO sessionDAO() {
return super.sessionDAO();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SessionManager sessionManager() {
return super.sessionManager();
}
@Bean
@ConditionalOnMissingBean
@Override
protected SessionsSecurityManager securityManager(List<Realm> realms) {
return super.securityManager(realms);
}
@Bean
@ConditionalOnResource(resources = "classpath:shiro.ini")
protected Realm iniClasspathRealm() {
return iniRealmFromLocation("classpath:shiro.ini");
}
@Bean
@ConditionalOnResource(resources = "classpath:META-INF/shiro.ini")
protected Realm iniMetaInfClasspathRealm() {
return iniRealmFromLocation("classpath:META-INF/shiro.ini");
}
@Bean
@ConditionalOnMissingBean(Realm.class)
protected Realm missingRealm() {
throw new NoRealmBeanConfiguredException();
}
}
大致內容其實和ShiroWebAutoConfiguration很類似,只是ShiroWebAutoConfiguration將一些組件替換成了WEB環境相關的組件。但是ShiroWebAutoConfiguration聲明了它的配置要在ShiroAutoConfiguration之前,而且根據ConditionalOnMissingBean的條件,得出Bean的配置應該是以ShiroWebAutoConfiguration中聲明的為准。但是死馬當活馬醫,配置文件中添加shiro.enabled為false的條件,再試試。。。果然還是不行。
四)DEBUG大法好
毫無辦法的辦法就是DEBUG大法。
首先從Configuration中生命的Bean是如何被容器加載的過程入手,找到了ConfigurationClassPostProcessor。同樣是一個PostProcessor,猜想應該是在configuration bean的后置處理中進行了@Bean方法的解析。
主要的處理過程在processConfigBeanDefinition這個方法中,對這個方法做個簡單的說明
/**
* Build and validate a configuration model based on the registry of
* {@link Configuration} classes.
*/
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
//獲取registry中的bean definition
String[] candidateNames = registry.getBeanDefinitionNames();
for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
//bean definition 有configuration的屬性,說明已經被解析處理過
if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||
ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {
if (logger.isDebugEnabled()) {
logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
}
}
//判斷是否是configuration的bean,是則加入候選
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}
// 如果沒有發現候選者,則返回
if (configCandidates.isEmpty()) {
return;
}
// 排序
configCandidates.sort((bd1, bd2) -> {
int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
return Integer.compare(i1, i2);
});
// Detect any custom bean name generation strategy supplied through the enclosing application context
SingletonBeanRegistry sbr = null;
if (registry instanceof SingletonBeanRegistry) {
sbr = (SingletonBeanRegistry) registry;
if (!this.localBeanNameGeneratorSet) {
BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(CONFIGURATION_BEAN_NAME_GENERATOR);
if (generator != null) {
this.componentScanBeanNameGenerator = generator;
this.importBeanNameGenerator = generator;
}
}
}
if (this.environment == null) {
this.environment = new StandardEnvironment();
}
// 開始解析configuration 的bean definition
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);
Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
// 如果候選者不為空,則繼續解析
do {
// 解析過程
parser.parse(candidates);
// 校驗
parser.validate();
// 獲取新解析的config class
Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
// 移除掉已經解析過的部分
configClasses.removeAll(alreadyParsed);
// 創建reader,添加bean definition
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);
candidates.clear();
//如果bean definition數量 大於 候選者的數量,說明有新的bean加入
if (registry.getBeanDefinitionCount() > candidateNames.length) {
String[] newCandidateNames = registry.getBeanDefinitionNames();
Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
Set<String> alreadyParsedClasses = new HashSet<>();
for (ConfigurationClass configurationClass : alreadyParsed) {
alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
}
for (String candidateName : newCandidateNames) {
//不在舊的candidate中,說明是新加入的
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
//未被解析的config class,添加到candidates中,等下一輪解析
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
!alreadyParsedClasses.contains(bd.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
}
}
}
//更新候選者
candidateNames = newCandidateNames;
}
}
while (!candidates.isEmpty());
// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
}
if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
// Clear cache in externally provided MetadataReaderFactory; this is a no-op
// for a shared cache since it'll be cleared by the ApplicationContext.
((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
}
}
1)parser.parse后設置斷點,看ConfigurationClassParser是否能將ShiroWebAutoConfiguration中的@Bean正常的解析:
可以看到authorizer確實已經被ShiroWebAutoConfiguration加載。
2)解析沒問題,那就看加載是否成功:
繼續往下走,看reader.loadBeanDefinitions發生了什么:
找出ShiroWebAutoConfiguration對應的ConfigurationClass,看到SkippedBeanMethods中有authorizer!!!也就是說雖然解析出了authorizer,但是在加載的時候卻被選擇跳過了。。。
3)問題就變得比較清晰了,找出為什么被跳過的原因。
順着代碼找到ConfigurationClassBeanDefinitionReader的loadBeanDefinitionsForConfigurationClass的方法,負責處理的BeanMethond的過程是在loadBeanDefitionsForBeanMethod中。
確實在方法開始前,有一個判斷是否需要跳過的條件:
if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
configClass.skippedBeanMethods.add(methodName);
return;
}
shouldSkip這個方法是根據@Bean上的@Conditional注解,來判斷是否需要加載該Bean。回憶上文我們的ShiroWebAutoConfiguration中,確實在authorizer的方法上有@ConditionalOnMissingBean的注解。也就是說應該是哪里聲明authorizer的Bean,導致配置中的Bean沒有被加載。
4)OnBeanCondition.getMatchOutcome():處理@Bean的@Condtional條件,並輸出結果。
最后發現被跳過的原因竟然是:
found beans of type 'org.apache.shiro.authz.Authorizer' authorizer, thirdPartyRealm, userRealm
我自定義的Realm竟然和authorizer沖突了。Spring認為已經有authorizer的bean,而不再加載配置中的authorizer。
5)為什么Realm和authorizer沖突?原來在獲取相匹配的Bean時候還是通過容器本身(BeanFactory)的getNamesForType方法:
Set<String> getNamesForType(Class<?> type) {
updateTypesIfNecessary();
//便利容器中所有的bean類型,將類型匹配的Type全部返回。注意這里還用了isAssiginableFrom,因此這里的查詢類型的子類也會滿足
return this.beanTypes.entrySet().stream()
.filter((entry) -> entry.getValue() != null
&& type.isAssignableFrom(entry.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
反觀我們的Realm對象:AuthorizingRealm實現了Authorizer接口。真相大白。