一)问题描述:
1我在一个Spring的项目中使用shiro搭建权限控制框架。主要通过shiro-spring-boot-web-starter包快速集成Shiro。但是项目无法启动,报没有authorizer的bean的错误: 2 3``` 4No bean named 'authorizer' available 5``` 6 7我只好又在自己的Configuration中又配置了Authorizer,才能正常启动。 8 9@Configuration 10public class ShiroConfig { 11 12@Bean 13public Authorizer authorizer(){ 14 return new ModularRealmAuthorizer(); 15} 16}
但是奇怪的明明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来达到集成自身配置的目的。
1org.springframework.boot.autoconfigure.EnableAutoConfiguration = \ 2org.apache.shiro.spring.config.web.autoconfigure.ShiroWebAutoConfiguration,\ 3org.apache.shiro.spring.config.web.autoconfigure.ShiroWebFilterConfiguration 4
上述声明了两个Configration:ShiroWebAutoConfiguration和ShiroWebFilterConfiguration。
-
ShiroWebFilterConfiguration 先从简单的配置说起,ShiroWebFilterConfiguration是以添加Filter的方式来达到authentication的目的。这个和我们的问题无关,简单带过。
-
ShiroWebAutoConfiguration
@Configuration @AutoConfigureBefore(ShiroAutoConfiguration.class) @ConditionalOnProperty(name = "shiro.web.enabled", matchIfMissing = true) public class ShiroWebAutoConfiguration extends AbstractShiroWebConfiguration {
1@Bean 2@ConditionalOnMissingBean 3@Override 4protected AuthenticationStrategy authenticationStrategy() { 5 return super.authenticationStrategy(); 6} 7 8@Bean 9@ConditionalOnMissingBean 10@Override 11protected Authenticator authenticator() { 12 return super.authenticator(); 13} 14 15@Bean 16@ConditionalOnMissingBean 17@Override 18protected Authorizer authorizer() { 19 return super.authorizer(); 20} 21 22@Bean 23@ConditionalOnMissingBean 24@Override 25protected SubjectDAO subjectDAO() { 26 return super.subjectDAO(); 27} 28 29@Bean 30@ConditionalOnMissingBean 31@Override 32protected SessionStorageEvaluator sessionStorageEvaluator() { 33 return super.sessionStorageEvaluator(); 34} 35 36@Bean 37@ConditionalOnMissingBean 38@Override 39protected SubjectFactory subjectFactory() { 40 return super.subjectFactory(); 41} 42 43@Bean 44@ConditionalOnMissingBean 45@Override 46protected SessionFactory sessionFactory() { 47 return super.sessionFactory(); 48} 49 50@Bean 51@ConditionalOnMissingBean 52@Override 53protected SessionDAO sessionDAO() { 54 return super.sessionDAO(); 55} 56 57@Bean 58@ConditionalOnMissingBean 59@Override 60protected SessionManager sessionManager() { 61 return super.sessionManager(); 62} 63 64@Bean 65@ConditionalOnMissingBean 66@Override 67protected SessionsSecurityManager securityManager(List<Realm> realms) { 68 return createSecurityManager(); 69} 70 71@Bean 72@ConditionalOnMissingBean(name = "sessionCookieTemplate") 73@Override 74protected Cookie sessionCookieTemplate() { 75 return super.sessionCookieTemplate(); 76} 77 78@Bean 79@ConditionalOnMissingBean 80@Override 81protected RememberMeManager rememberMeManager() { 82 return super.rememberMeManager(); 83} 84 85@Bean 86@ConditionalOnMissingBean(name = "rememberMeCookieTemplate") 87@Override 88protected Cookie rememberMeCookieTemplate() { 89 return super.rememberMeCookieTemplate(); 90} 91 92@Bean 93@ConditionalOnMissingBean 94@Override 95protected ShiroFilterChainDefinition shiroFilterChainDefinition() { 96 return super.shiroFilterChainDefinition(); 97}}
这个配置类将Shiro需要的各组件都声明成了bean,交给容器管理。具体的创建过程都在父类AbstractShiroWebConfiguration。可以看到确实是有声明authorizer。但是为什么会找不到呢?是不是其他的配置文件声明了类似的bean,产生了影响?
三)继续查找其他配置
观察shiro-spring-boot-web-starter的配置文件,可以看到它又引用了shiro-spring-boot-starter包。shrio-spring-boot-starter又是一个Spring boot starter包,同样通过它的META-INF文件,可以知道加入了哪些Configuration:
1org.springframework.boot.autoconfigure.EnableAutoConfiguration = \ 2 org.apache.shiro.spring.boot.autoconfigure.ShiroBeanAutoConfiguration,\ 3 org.apache.shiro.spring.boot.autoconfigure.ShiroAutoConfiguration,\ 4 org.apache.shiro.spring.boot.autoconfigure.ShiroAnnotationProcessorAutoConfiguration 5 6org.springframework.boot.diagnostics.FailureAnalyzer = \ 7 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 {
1@Bean 2@ConditionalOnMissingBean 3@Override 4protected AuthenticationStrategy authenticationStrategy() { 5 return super.authenticationStrategy(); 6} 7 8@Bean 9@ConditionalOnMissingBean 10@Override 11protected Authenticator authenticator() { 12 return super.authenticator(); 13} 14 15@Bean 16@ConditionalOnMissingBean 17@Override 18protected Authorizer authorizer() { 19 return super.authorizer(); 20} 21 22@Bean 23@ConditionalOnMissingBean 24@Override 25protected SubjectDAO subjectDAO() { 26 return super.subjectDAO(); 27} 28 29@Bean 30@ConditionalOnMissingBean 31@Override 32protected SessionStorageEvaluator sessionStorageEvaluator() { 33 return super.sessionStorageEvaluator(); 34} 35 36@Bean 37@ConditionalOnMissingBean 38@Override 39protected SubjectFactory subjectFactory() { 40 return super.subjectFactory(); 41} 42 43@Bean 44@ConditionalOnMissingBean 45@Override 46protected SessionFactory sessionFactory() { 47 return super.sessionFactory(); 48} 49 50@Bean 51@ConditionalOnMissingBean 52@Override 53protected SessionDAO sessionDAO() { 54 return super.sessionDAO(); 55} 56 57@Bean 58@ConditionalOnMissingBean 59@Override 60protected SessionManager sessionManager() { 61 return super.sessionManager(); 62} 63 64@Bean 65@ConditionalOnMissingBean 66@Override 67protected SessionsSecurityManager securityManager(List<Realm> realms) { 68 return super.securityManager(realms); 69} 70 71@Bean 72@ConditionalOnResource(resources = "classpath:shiro.ini") 73protected Realm iniClasspathRealm() { 74 return iniRealmFromLocation("classpath:shiro.ini"); 75} 76 77@Bean 78@ConditionalOnResource(resources = "classpath:META-INF/shiro.ini") 79protected Realm iniMetaInfClasspathRealm() { 80 return iniRealmFromLocation("classpath:META-INF/shiro.ini"); 81} 82 83@Bean 84@ConditionalOnMissingBean(Realm.class) 85protected Realm missingRealm() { 86 throw new NoRealmBeanConfiguredException(); 87}}
大致内容其实和ShiroWebAutoConfiguration很类似,只是ShiroWebAutoConfiguration将一些组件替换成了WEB环境相关的组件。但是ShiroWebAutoConfiguration声明了它的配置要在ShiroAutoConfiguration之前,而且根据ConditionalOnMissingBean的条件,得出Bean的配置应该是以ShiroWebAutoConfiguration中声明的为准。但是死马当活马医,配置文件中添加shiro.enabled为false的条件,再试试。。。果然还是不行。
四)DEBUG大法好
毫无办法的办法就是DEBUG大法。 首先从Configuration中生命的Bean是如何被容器加载的过程入手,找到了ConfigurationClassPostProcessor。同样是一个PostProcessor,猜想应该是在configuration bean的后置处理中进行了@Bean方法的解析。 主要的处理过程在processConfigBeanDefinition这个方法中,对这个方法做个简单的说明
1/** 2 * Build and validate a configuration model based on the registry of 3 * {@link Configuration} classes. 4 */ 5 public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { 6 List<BeanDefinitionHolder> configCandidates = new ArrayList<>(); 7 //获取registry中的bean definition 8 String[] candidateNames = registry.getBeanDefinitionNames(); 9 10 for (String beanName : candidateNames) { 11 BeanDefinition beanDef = registry.getBeanDefinition(beanName); 12 //bean definition 有configuration的属性,说明已经被解析处理过 13 if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) || 14 ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) { 15 if (logger.isDebugEnabled()) { 16 logger.debug("Bean definition has already been processed as a configuration class: " + beanDef); 17 } 18 } 19 //判断是否是configuration的bean,是则加入候选 20 else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) { 21 configCandidates.add(new BeanDefinitionHolder(beanDef, beanName)); 22 } 23 } 24 25 // 如果没有发现候选者,则返回 26 if (configCandidates.isEmpty()) { 27 return; 28 } 29 30 // 排序 31 configCandidates.sort((bd1, bd2) -> { 32 int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition()); 33 int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition()); 34 return Integer.compare(i1, i2); 35 }); 36 37 // Detect any custom bean name generation strategy supplied through the enclosing application context 38 SingletonBeanRegistry sbr = null; 39 if (registry instanceof SingletonBeanRegistry) { 40 sbr = (SingletonBeanRegistry) registry; 41 if (!this.localBeanNameGeneratorSet) { 42 BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(CONFIGURATION_BEAN_NAME_GENERATOR); 43 if (generator != null) { 44 this.componentScanBeanNameGenerator = generator; 45 this.importBeanNameGenerator = generator; 46 } 47 } 48 } 49 50 if (this.environment == null) { 51 this.environment = new StandardEnvironment(); 52 } 53 54 // 开始解析configuration 的bean definition 55 ConfigurationClassParser parser = new ConfigurationClassParser( 56 this.metadataReaderFactory, this.problemReporter, this.environment, 57 this.resourceLoader, this.componentScanBeanNameGenerator, registry); 58 59 Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates); 60 Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size()); 61 62 // 如果候选者不为空,则继续解析 63 do { 64 // 解析过程 65 parser.parse(candidates); 66 // 校验 67 parser.validate(); 68 69 // 获取新解析的config class 70 Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses()); 71 // 移除掉已经解析过的部分 72 configClasses.removeAll(alreadyParsed); 73 74 // 创建reader,添加bean definition 75 if (this.reader == null) { 76 this.reader = new ConfigurationClassBeanDefinitionReader( 77 registry, this.sourceExtractor, this.resourceLoader, this.environment, 78 this.importBeanNameGenerator, parser.getImportRegistry()); 79 } 80 this.reader.loadBeanDefinitions(configClasses); 81 alreadyParsed.addAll(configClasses); 82 83 candidates.clear(); 84 //如果bean definition数量 大于 候选者的数量,说明有新的bean加入 85 if (registry.getBeanDefinitionCount() > candidateNames.length) { 86 String[] newCandidateNames = registry.getBeanDefinitionNames(); 87 Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames)); 88 89 Set<String> alreadyParsedClasses = new HashSet<>(); 90 91 for (ConfigurationClass configurationClass : alreadyParsed) { 92 alreadyParsedClasses.add(configurationClass.getMetadata().getClassName()); 93 } 94 95 for (String candidateName : newCandidateNames) { 96 //不在旧的candidate中,说明是新加入的 97 if (!oldCandidateNames.contains(candidateName)) { 98 99 BeanDefinition bd = registry.getBeanDefinition(candidateName); 100 //未被解析的config class,添加到candidates中,等下一轮解析 101 if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) && 102 !alreadyParsedClasses.contains(bd.getBeanClassName())) { 103 candidates.add(new BeanDefinitionHolder(bd, candidateName)); 104 } 105 } 106 } 107 //更新候选者 108 candidateNames = newCandidateNames; 109 } 110 } 111 while (!candidates.isEmpty()); 112 113 // Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes 114 if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) { 115 sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry()); 116 } 117 118 if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) { 119 // Clear cache in externally provided MetadataReaderFactory; this is a no-op 120 // for a shared cache since it'll be cleared by the ApplicationContext. 121 ((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache(); 122 } 123 }
1)parser.parse后设置断点,看ConfigurationClassParser是否能将ShiroWebAutoConfiguration中的@Bean正常的解析:

可以看到authorizer确实已经被ShiroWebAutoConfiguration加载。
2)解析没问题,那就看加载是否成功: 继续往下走,看reader.loadBeanDefinitions发生了什么:

找出ShiroWebAutoConfiguration对应的ConfigurationClass,看到SkippedBeanMethods中有authorizer!!!也就是说虽然解析出了authorizer,但是在加载的时候却被选择跳过了。。。
3)问题就变得比较清晰了,找出为什么被跳过的原因。
顺着代码找到ConfigurationClassBeanDefinitionReader的loadBeanDefinitionsForConfigurationClass的方法,负责处理的BeanMethond的过程是在loadBeanDefitionsForBeanMethod中。
确实在方法开始前,有一个判断是否需要跳过的条件:
1if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) { 2 configClass.skippedBeanMethods.add(methodName); 3 return; 4 }
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方法:
1 Set<String> getNamesForType(Class<?> type) { 2 updateTypesIfNecessary(); 3 //便利容器中所有的bean类型,将类型匹配的Type全部返回。注意这里还用了isAssiginableFrom,因此这里的查询类型的子类也会满足 4 return this.beanTypes.entrySet().stream() 5 .filter((entry) -> entry.getValue() != null 6 && type.isAssignableFrom(entry.getValue())) 7 .map(Map.Entry::getKey) 8 .collect(Collectors.toCollection(LinkedHashSet::new)); 9 }
反观我们的Realm对象:AuthorizingRealm实现了Authorizer接口。真相大白。