需求说明
本人基于Springboot设计并开发了一套Cargo的配置系统,其中有一个比较核心的SDK模块会通过maven依赖进第三方项目中。但是第三方项目有可能是没有Spring环境的项目,并且如何保证在不同的项目上能够在项目启动的时候就能够执行Cargo的拦截器方法来执行相应的Cargo日志采集或者其他的操作。
第一版的做法是让第三方项目的开发同学在项目启动阶段的地方手动的写一句@ServletComponentScan(basePackageClasses = XXXFilter.class)来使用拦截器操作。但是这句代码还是造成了一定的业务侵入了。最好的做法当然是考虑对第三方项目的业务代码完全无侵入(连一行配置性的代码都不需要写)的方案,使得对接入方使用更加友好。
总结一下要解决的痛点就是:
1、如何解决对第三方项目的配置侵入
2、如何作到当第三方项目启动的时候能够让我的Cargo也启动起来
3、如何能够屏蔽第三方项目是否有Spring环境
设计思路和方案
1、制作starter
SpringBoot的starter机制的原理是:starter是springboot的核心机制,springboot会扫描所有的spring.factoies文件,读取里面的内容就能够定位到EnableAutoConfiguration的类是哪一个java类。然后去执行该类的代码(解析注解和执行代码逻辑)。
代码实现:
1、在client层的resources/META-INF/下新建一个spring.factoies的配置文件,告诉SpringBoot:请把我整个Cargo当成你的一个组件,同时会找到CargoFilterAutoConfiguration类并去解析该类。不需要第三方项目的研发去写启动Cargo的配置性代码,解决了痛点1和痛点2。
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ctrip.ibu.cargo.filter.CargoFilterAutoConfiguration
2、Springboot解析CargoFilterAutoConfiguration 类时候根据@Configuration发现它是一个核心配置类,根据@Condition开头的注解启动自动配置功能。在CargoFilterAutoConfiguration类中执行FilterRegistrationBean的相关逻辑就顺利的启动了filter功能。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import javax.servlet.DispatcherType;
@Configuration
public class CargoFilterAutoConfiguration {
@Configuration
@Conditional(CargoNoneFilterCondition.class)
static class CargoFilterConfigurationNew {
@Bean(name = "CargoFilterRegistrationBeanNew")
public org.springframework.boot.web.servlet.FilterRegistrationBean factory() {
org.springframework.boot.web.servlet.FilterRegistrationBean filter =
new org.springframework.boot.web.servlet.FilterRegistrationBean();
filter.setFilter(new CargoFilter());
filter.setName("cargo-filter");
filter.addUrlPatterns("/*");
filter.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD);
filter.setAsyncSupported(true);
filter.setOrder(Ordered.HIGHEST_PRECEDENCE);
return filter;
}
}
@Configuration
@Conditional(CargoFilterCondition.class)
static class CargoFilterConfigurationOld {
@Bean(name = "CargoFilterRegistrationBeanOld")
public org.springframework.boot.context.embedded.FilterRegistrationBean factory() {
org.springframework.boot.context.embedded.FilterRegistrationBean filter =
new org.springframework.boot.context.embedded.FilterRegistrationBean();
filter.setFilter(new CargoFilter());
filter.setName("cargo-filter");
filter.addUrlPatterns("/*");
filter.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD);
filter.setAsyncSupported(true);
filter.setOrder(Ordered.HIGHEST_PRECEDENCE);
return filter;
}
}
}
然后配置具体的条件组合,来决定走哪一个factory()方法。
public class CargoNoneFilterCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
try {
Class.forName("org.springframework.boot.context.embedded.FilterRegistrationBean");
return false;
}catch (Throwable e){
try{
Class.forName("org.springframework.boot.web.servlet.FilterRegistrationBean");
return true;
}catch (Throwable e1){
return false;
}
}
}
}
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class CargoFilterCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
try {
Class.forName("org.springframework.boot.context.embedded.FilterRegistrationBean");
return true;
}catch (Throwable e){
return false;
}
}
}
上面也可以将实现Condition接口的方式改写成@ConditionXXX的组合注解的方式,没有本质区别,看个人喜好。
其中CargoNoneFilterCondition类等价于:
@ConditionOnClass("org.springframework.boot.web.servlet.FilterRegistrationBean")
@ConditionOnMissClass("org.springframework.boot.context.embedded.FilterRegistrationBean")
CargoFilterCondition类等价于:
@ConditionOnClass("org.springframework.boot.context.embedded.FilterRegistrationBean")
4、在factory()中会去执行CargoFilter中的代码,做具体的Filter的业务逻辑。
package com.ctrip.ibu.cargo.filter;
import com.ctrip.ibu.cargo.context.CargoContext;
import com.ctrip.ibu.cargo.context.ContextInfo;
import com.ctrip.ibu.cargo.context.ContextParser;
import com.ctrip.ibu.cargo.entity.Currency;
import com.ctrip.ibu.cargo.entity.Locale;
import com.ctrip.ibu.cargo.entity.Site;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* Cargo filter for Online, which will put the thread context info.
*/
@WebFilter(urlPatterns = "/*", filterName = "cargo-filter", description = "Cargo Filter to put the thread context info.")
public class CargoFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(CargoFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
try {
ContextInfo contextInfo = ContextParser.parse((HttpServletRequest) servletRequest);
if (contextInfo != null) {
CargoContext.set(CargoContext.HOST, contextInfo.getHost());
if (contextInfo.getSite() != null){
CargoContext.set(CargoContext.DOMAIN, contextInfo.getDomain());
CargoContext.set(CargoContext.LANGUAGE, contextInfo.getLanguage());
CargoContext.set(Currency.class, contextInfo.getCurrency());
CargoContext.set(Locale.class, contextInfo.getLocale());
CargoContext.set(Site.class, contextInfo.getSite());
}
}
chain.doFilter(servletRequest, servletResponse);
} catch (Exception ex) {
log.error("An exception occurred when set the current thread context.", ex);
} finally {
CargoContext.clear();
}
}
@Override
public void destroy() {
}
}
代码分析:
通过@ConditionXXX的组合条件的结果(或者实现Condition接口的matches方法的执行结果)来选择是执行CargoFilterConfigurationNew的factory方法还是CargoFilterConfigurationOld的factory方法,对应的其实是无Spring环境的拦截器和有spring环境的拦截器。在factory内执行filter的逻辑就可以顺利的启动了Cargo的filter功能。这样就解决了痛点3。
下面是关于原理部分的华丽丽的分割线。
原理方面的源代码分析:
SpringBoot的自动化配置原理,首先是从@SpringBootApplication开始的,这个注解是一个组合注解,核心功能时由@EnableAutoConfiguration提供的。观察@EnableAutoConfiguration可以发现,这里Import了@EnableAutoConfigurationImportSelector,这就是Spring Boot自动化配置的“始作俑者”。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
/**
* Exclude specific auto-configuration classes such that they will never be applied.
* @return the classes to exclude
*/
Class<?>[] exclude() default {};
/**
* Exclude specific auto-configuration class names such that they will never be
* applied.
* @return the class names to exclude
* @since 1.3.0
*/
String[] excludeName() default {};
}
一路debug过来(最上面的堆栈就是getCandidateConfigurations的代码了)。
EnableAutoConfigurationImportSelector的关键代码在这里:(上面我实际运用的例子中我配置了:org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.ctrip.ibu.cargo.filter.CargoFilterAutoConfiguration,所以CargoFilterAutoConfiguration类就会被springboot认识了。)
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
接下来再细致的讲解一下springboot的执行过程:(不仅限于autoConfiguration本身,还包括接下来如何生效)
1、首先Springboot框架会扫描业务代码中所有继承SpringBootServletInitializer的类,然后执行SpringApplication.run(XXXServiceInitializer.class);方法。用过Springboot的人都知道这个,我就不多展开说明了。
2、然后会执行SpringApplication的refreshContext方法,里面有个initialize(sources);方法。initialize中会解析上面写的这个spring.factories配置文件(参见2.2),然后生成若干个XXXInitialzers和XXXListeners的类的实例。
@SuppressWarnings({ "unchecked", "rawtypes" })
private void initialize(Object[] sources) {
if (sources != null && sources.length > 0) {
this.sources.addAll(Arrays.asList(sources));
}
this.webEnvironment = deduceWebEnvironment();
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
2.1、getSpringFactoriesInstances方法执行loadFactoryNames方法然后通过反射生成配置文件中若干个value所对应的类的实例。
private <T> Collection<? extends T> getSpringFactoriesInstances(Class<T> type,
Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// Use names and ensure unique to protect against duplicates
Set<String> names = new LinkedHashSet<String>(
SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
2.2、其中loadFactoryNames方法如下:SpringFactoriesLoader类会去项目的META-INF/spring.factories路径下去寻找这个spring.factories配置文件,然后加以解析。
/**
* The location to look for factories.
* <p>Can be present in multiple JAR files.
*/
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
try {
Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
List<String> result = new ArrayList<String>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
String factoryClassNames = properties.getProperty(factoryClassName);
result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
}
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() +
"] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
3、以上事情都做完之后会根据之前生成的Listener来发一系列的事件。【这是很经典的一种观察者模式(又称为发布-订阅模式),值得学习。有助于写出很棒的低耦合高内聚的很棒的代码,脱离草根程序员的level】,会有一个后置处理器来利用之前实例化好的类来真正做事情。
public void environmentPrepared(ConfigurableEnvironment environment) {
for (SpringApplicationRunListener listener : this.listeners) {
listener.environmentPrepared(environment);
}
}
4、经历了一堆的代码之后,CargoFilterAutoConfiguration类processConfigBeanDefinitions方法会将之前注册的拿来执行
/**
* Derive further bean definitions from the configuration classes in the registry.
*/
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
RootBeanDefinition iabpp = new RootBeanDefinition(ImportAwareBeanPostProcessor.class);
iabpp.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(IMPORT_AWARE_PROCESSOR_BEAN_NAME, iabpp);
RootBeanDefinition ecbpp = new RootBeanDefinition(EnhancedConfigurationBeanPostProcessor.class);
ecbpp.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(ENHANCED_CONFIGURATION_PROCESSOR_BEAN_NAME, ecbpp);
int registryId = System.identityHashCode(registry);
if (this.registriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
}
if (this.factoriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanFactory already called on this post-processor against " + registry);
}
this.registriesPostProcessed.add(registryId);
processConfigBeanDefinitions(registry);
}
/**
* Build and validate a configuration model based on the registry of
* {@link Configuration} classes.
*/
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<BeanDefinitionHolder>();
String[] candidateNames = registry.getBeanDefinitionNames();
for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||
ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {
if (logger.isDebugEnabled()) {
logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
}
}
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}
// Return immediately if no @Configuration classes were found
if (configCandidates.isEmpty()) {
return;
}
// Sort by previously determined @Order value, if applicable
Collections.sort(configCandidates, new Comparator<BeanDefinitionHolder>() {
@Override
public int compare(BeanDefinitionHolder bd1, BeanDefinitionHolder bd2) {
int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
return (i1 < i2) ? -1 : (i1 > i2) ? 1 : 0;
}
});
// Detect any custom bean name generation strategy supplied through the enclosing application context
SingletonBeanRegistry singletonRegistry = null;
if (registry instanceof SingletonBeanRegistry) {
singletonRegistry = (SingletonBeanRegistry) registry;
if (!this.localBeanNameGeneratorSet && singletonRegistry.containsSingleton(CONFIGURATION_BEAN_NAME_GENERATOR)) {
BeanNameGenerator generator = (BeanNameGenerator) singletonRegistry.getSingleton(CONFIGURATION_BEAN_NAME_GENERATOR);
this.componentScanBeanNameGenerator = generator;
this.importBeanNameGenerator = generator;
}
}
// Parse each @Configuration class
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);
Set<BeanDefinitionHolder> candidates = new LinkedHashSet<BeanDefinitionHolder>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<ConfigurationClass>(configCandidates.size());
do {
parser.parse(candidates);
parser.validate();
Set<ConfigurationClass> configClasses = new LinkedHashSet<ConfigurationClass>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);
// Read the model and create bean definitions based on its content
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();
if (registry.getBeanDefinitionCount() > candidateNames.length) {
String[] newCandidateNames = registry.getBeanDefinitionNames();
Set<String> oldCandidateNames = new HashSet<String>(Arrays.asList(candidateNames));
Set<String> alreadyParsedClasses = new HashSet<String>();
for (ConfigurationClass configurationClass : alreadyParsed) {
alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
}
for (String candidateName : newCandidateNames) {
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition beanDef = registry.getBeanDefinition(candidateName);
if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory) &&
!alreadyParsedClasses.contains(beanDef.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(beanDef, candidateName));
}
}
}
candidateNames = newCandidateNames;
}
}
while (!candidates.isEmpty());
// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
if (singletonRegistry != null) {
if (!singletonRegistry.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
singletonRegistry.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
}
}
if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
}
}
ConfigurationClassParser类执行doProcessConfigurationClass方法,该方法会循环递归的调用自己。完成springboot配置发挥作用的业务逻辑。限于篇幅且比较复杂就不再赘述。
以上的源代码逻辑就讲解完毕。
衡量是否真的掌握一个知识的最好方式就是看你是否能够将你知道的这部分知识灵活的运用起来,当你做到这一点然后你会发现越来越有成就感。一起加油吧!!!