读取spring.factories文件与监听器

By | 2021年12月31日

spring.factories

1 Spring Factories机制

在日常工作中,我们可能需要实现一些SDK或者Spring Boot Starter给被人使用,这个使用我们就可以使用Factories机制。
Factories机制可以让SDK或者Starter的使用只需要很少或者不需要进行配置,只需要在服务中引入我们的jar包。

例如,CAS 开源项目里,有个 resources\META-INF\spring.factories 文件。若我们想在此项目中创建一个自己的 myFilter,那么不能用@Component注解了,但可以将这个 myFilter 名称放入 spring.factories 来使这个Filter生效,SpringFactoriesLoader 会加载这个Filter。

2 META-INF/spring.factories 文件内容

Spring提供了 SpringFactoriesLoader 类,用于读取 spring.factories 文件内容。
我们先看下 SpringBoot 2.0.3中,META-INF/spring.factories 文件内容:

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor

# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.diagnostics.analyzer.BeanCurrentlyInCreationFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BeanNotOfRequiredTypeFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BindFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BindValidationFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.UnboundConfigurationPropertyFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.ConnectorStartFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.PortInUseFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.InvalidConfigurationPropertyNameFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.InvalidConfigurationPropertyValueFailureAnalyzer

# FailureAnalysisReporters
org.springframework.boot.diagnostics.FailureAnalysisReporter=\
org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter

我们将关注点放置到上述文件的Application Listeners的定义中,key为org.springframework.context.ApplicationListener,通过下面方式可以读取到这10个Listener(其中一个 BackgroundPreinitializer 不在上面的文件中):

public class SpringFactoriesLoaderTest {
    public static void main(String[] args) throws IOException {
        List<ApplicationListener> loadFactories = SpringFactoriesLoader.loadFactories(ApplicationListener.class, null);
        System.out.println(loadFactories.size());
    }
}

3 SpringFactoriesLoader.loadFactories 源码

public static <T> List<T> loadFactories(Class<T> factoryClass, @Nullable ClassLoader classLoader) {
        Assert.notNull(factoryClass, "'factoryClass' must not be null");
        ClassLoader classLoaderToUse = classLoader;
        if(classLoader == null) {
            classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
        }

        List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse);
        if(logger.isTraceEnabled()) {
            logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames);
        }

        List<T> result = new ArrayList(factoryNames.size());
        Iterator var5 = factoryNames.iterator();

        while(var5.hasNext()) {
            String factoryName = (String)var5.next();
            result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse));
        }

        AnnotationAwareOrderComparator.sort(result);
        return result;
}

上述代码,首先获取类加载器,然后调用loadFactoryNames方法进行操作,然后开始遍历所有的factoryNames并调用instantiateFactory方法实例化这些类,最后调用AnnotationAwareOrderComparator.sort进行排序。我们将关注点放置到loadFactoryNames方法的处理逻辑中。

4 SpringFactoriesLoader.loadFactoryNames方法

public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
        String factoryClassName = factoryClass.getName();
        return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
    if(result != null) {
        return result;
    } else {
        try {
            Enumeration<URL> urls = classLoader != null?classLoader.getResources("META-INF/spring.factories"):ClassLoader.getSystemResources("META-INF/spring.factories");
            LinkedMultiValueMap result = new LinkedMultiValueMap();

            while(urls.hasMoreElements()) {
                URL url = (URL)urls.nextElement();
                UrlResource resource = new UrlResource(url);
                Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                Iterator var6 = properties.entrySet().iterator();

                while(var6.hasNext()) {
                    Entry<?, ?> entry = (Entry)var6.next();
                    List<String> factoryClassNames = Arrays.asList(StringUtils.commaDelimitedListToStringArray((String)entry.getValue()));
                    result.addAll((String)entry.getKey(), factoryClassNames);
                }
            }

            cache.put(classLoader, result);
            return result;
        } catch (IOException var9) {
            throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var9);
        }
    }
}

上述代码中,有一段:Enumeration<URL> urls = classLoader != null?classLoader.getResources(“META-INF/spring.factories”):ClassLoader.getSystemResources(“META-INF/spring.factories”); 可见,默认读取的文件是META-INF/spring.factories。

5 PropertiesLoaderUtils.loadProperties

public static Properties loadProperties(Resource resource) throws IOException {
    Properties props = new Properties();
    fillProperties(props, resource);
    return props;
}

public static void fillProperties(Properties props, Resource resource) throws IOException {
    InputStream is = resource.getInputStream();

    try {
        String filename = resource.getFilename();
        if(filename != null && filename.endsWith(".xml")) {
            props.loadFromXML(is);
        } else {
            props.load(is);
        }
    } finally {
        is.close();
    }

}

上述代码,首先实例化Properties 类,然后调用fillProperties方法进行处理。如果文件的后缀是.xml使用props.loadFromXML(is);方法,否则使用props.load(is),非常简单。

Listener监听器

监听器是一个专门用于对其他对象身上发生的事件或状态改变进行监听和相应处理的对象,当被监视的对象发生情况时,立即采取相应的行动。

1 ServletRequestListener

此处我们以ServletRequestListener接口来说,里面有两个方法,分别是请求创建和请求销毁。

@WebListener
public class RequestListenter implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
        System.out.println(">>> 请求销毁");
    }
 
    @Override
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {
        System.out.println(">>> 请求创建");
    }
}

注意:@WebListener注解为声明此类为Listener,无需再进行配置,唯一注意的是,使用注解的方式声明Listener时,需要再main函数类上添加@ServletComponentScan(basePackages = “此处写明类地址,格式为包名+类名”)。

2 SingleSignOutHttpSessionListener

我们再看下 cas-client-core-3.4.1.jar 中的 SingleSignOutHttpSessionListener.class的源码:

package org.jasig.cas.client.session;

import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

/**
 * Listener to detect when an HTTP session is destroyed and remove it from the map of
 * managed sessions.  Also allows for the programmatic removal of sessions.
 * <p>
 * Enables the CAS Single Sign out feature.
 *
 * Scott Battaglia
 * @version $Revision$ Date$
 * @since 3.1
 */
public final class SingleSignOutHttpSessionListener implements HttpSessionListener {

    private SessionMappingStorage sessionMappingStorage;

    public void sessionCreated(final HttpSessionEvent event) {
        // nothing to do at the moment
    }

    public void sessionDestroyed(final HttpSessionEvent event) {
        if (sessionMappingStorage == null) {
            sessionMappingStorage = getSessionMappingStorage();
        }
        final HttpSession session = event.getSession();
        sessionMappingStorage.removeBySessionById(session.getId());
    }

    /**
     * Obtains a {@link SessionMappingStorage} object. Assumes this method will always return the same
     * instance of the object.  It assumes this because it generally lazily calls the method.
     * 
     * @return the SessionMappingStorage
     */
    protected static SessionMappingStorage getSessionMappingStorage() {
        return SingleSignOutFilter.getSingleSignOutHandler().getSessionMappingStorage();
    }
}

这个监听器上没有使用@WebListener,目前应该没有自动启用,该如何启用它呢?很简单,创建一个 ServletListenerRegistrationBean就可以了,请看下面的CasConfiguration.class:

@Bean
public ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> singleSignOutHttpSessionListener() {
    ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> listener = new ServletListenerRegistrationBean<SingleSignOutHttpSessionListener>();
    listener.setEnabled(true);
    listener.setListener(new SingleSignOutHttpSessionListener());
    listener.setOrder(1);
    return listener;
}

发表评论

您的电子邮箱地址不会被公开。