SpringBoot之自定义Starter

Springboot之自定义Starter

一、简介

Springboot中包含很多starter(启动器),来对应不同的使用场景,大大简化了我们日常使用的技术整合难度,只需要简单的配置就可以使用各种场景。但是Springboot并不能包含所有我们遇到的场景,Springboot支持我们自定义自己的场景启动器。

二、Springboot中的自动配置类

我们先看一下springboot的一个自动配置类

// Java的一个配置类
@Configuration  
// 当前项目是Web项目的条件下
@ConditionalOnWebApplication(type = Type.SERVLET)
// 当类路径classpath下有指定的类的情况下进行自动配置
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
// 当Spring Context中不存在该Bean时
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
// 自定配置类的优先级
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
// 需要在DispatcherServletAutoConfiguration等自动化配置之后进行配置,因为这些类的支持
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
    
    @Bean
	@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
    // 配置文件中的 spring.mvc.hiddenmethod.filter = true 时
	@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = true)
	public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
		return new OrderedHiddenHttpMethodFilter();
	}

	@Bean
	@ConditionalOnMissingBean(FormContentFilter.class)
	@ConditionalOnProperty(prefix = "spring.mvc.formcontent.filter", name = "enabled", matchIfMissing = true)
	public OrderedFormContentFilter formContentFilter() {
		return new OrderedFormContentFilter();
	}
    
    // .....  省略其他的部分 ....... 
}

相关注解含义解释

注解名 含义
@ConditionalOnClass 当类路径classpath下有指定的类的情况下进行自动配置
@ConditionalOnMissingBean 当容器(Spring Context)中没有指定Bean的情况下进行自动配置
@ConditionalOnProperty(prefix = “example.service”, value = “enabled”, matchIfMissing = true) 当配置文件中example.service.enabled=true时进行自动配置,如果没有设置此值就默认使用matchIfMissing对应的值
@ConditionalOnBean 当容器(Spring Context)中有指定的Bean的条件下
@ConditionalOnMissingClass 当类路径下没有指定的类的条件下
@ConditionalOnExpression 基于SpEL表达式作为判断条件
@ConditionalOnJava 基于JVM版本作为判断条件
@ConditionalOnJndi JNDI存在的条件下查找指定的位置
@ConditionalOnNotWebApplication 当前项目不是Web项目的条件下
@ConditionalOnWebApplication 当前项目是Web项目的条件下
@ConditionalOnResource 类路径下是否有指定的资源
@ConditionalOnSingleCandidate 当指定的Bean在容器中只有一个,或者在有多个Bean的情况下,用来指定首选的Bean

三、Springboot的自动配置的原理

自定义starter,我们想要彻底理解内部含义,就必须的明白什么是自动配置原理。

1. 首先看一下@SpringBootApplication注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

	@AliasFor(annotation = EnableAutoConfiguration.class)
	Class<?>[] exclude() default {};

	@AliasFor(annotation = EnableAutoConfiguration.class)
	String[] excludeName() default {};

	@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
	String[] scanBasePackages() default {};

	@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
	Class<?>[] scanBasePackageClasses() default {};

}
  • @SpringBootConfiguration:我们点进去以后可以发现底层是Configuration注解,说白了就是支持JavaConfig的方式来进行配置(使用Configuration配置类等同于XML文件)。
  • @EnableAutoConfiguration:开启自动配置功能
  • @ComponentScan:这个注解,学过Spring的同学应该对它不会陌生,就是扫描注解,默认是扫描当前类下的package。将@Controller/@Service/@Component/@Repository等注解加载到IOC容器中。
2. 接着看一下@EnableAutoConfiguration注解

Springboot减少了原来的SSM等项目中的xml配置信息,约定大于配置,主要归功于@EnableAutoConfiguration注解。这个注解可以自动载入应用程序所需要的所有默认配置。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

	String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

	Class<?>[] exclude() default {};

	String[] excludeName() default {};

}

其中有两个重要的注解,@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)

  • @AutoConfigurationPackage:自动配置包
  • @Import(AutoConfigurationImportSelector.class):给IOC容器导入组件。
3. 然后看一下AutoConfigurationPackage
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {

}

其中重要的是AutoConfigurationPackages.Registrar.class, 默认情况下就是将:主配置类@SpringBootApplication的所在的包及其子包里面的组件扫描到Spring容器中。但是我们日常使用的@Controller/@Service/@Component/@Repository这些注解是由ComponentScan来扫描并加载的。实体类上写@Entity注解。这个@Entity注解由@AutoConfigurationPackage扫描并加载。扫描的对象不一样。

下面是AutoConfigurationPackages.Registrar中关键代码:

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

    // 这个将类注入Spring容器
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        register(registry, new PackageImport(metadata).getPackageName());
    }

    @Override
    public Set<Object> determineImports(AnnotationMetadata metadata) {
        return Collections.singleton(new PackageImport(metadata));
    }

}
4. 返回到AutoConfigurationImportSelector类查看

其中的关键代码:

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
		ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
       
    // ........   省略  ..........        
	/**
	 * Return the auto-configuration class names that should be considered. By default
	 * this method will load candidates using {@link SpringFactoriesLoader} with
	 * {@link #getSpringFactoriesLoaderFactoryClass()}.
	 * @param metadata the source metadata
	 * @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
	 * attributes}
	 * @return a list of candidate configurations
	 */
	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;
	}
            
    // ........   省略  ..........         
}

接着进入loadFactoryNames方法中:

public final class SpringFactoriesLoader {
    
    public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
		String factoryClassName = factoryClass.getName();
		return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
	}
    
    private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
		MultiValueMap<String, String> result = cache.get(classLoader);
		if (result != null) {
			return result;
		}
		// FACTORIES_RESOURCE_LOCATION 就是 META-INF/spring.factories
		try {
			Enumeration<URL> urls = (classLoader != null ?
					classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
					ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
			result = new LinkedMultiValueMap<>();
			while (urls.hasMoreElements()) {
				URL url = urls.nextElement();
				UrlResource resource = new UrlResource(url);
				Properties properties = PropertiesLoaderUtils.loadProperties(resource);
				for (Map.Entry<?, ?> entry : properties.entrySet()) {
					String factoryClassName = ((String) entry.getKey()).trim();
					for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
						result.add(factoryClassName, factoryName.trim());
					}
				}
			}
			cache.put(classLoader, result);
			return result;
		}
		catch (IOException ex) {
			throw new IllegalArgumentException("Unable to load factories from location [" +
					FACTORIES_RESOURCE_LOCATION + "]", ex);
		}
	}
}

可以看出加载的是META-INF/spring.factories

5. 最后看一下spring.factories文件

在看一下spring中的自动配置类吧,真的好多啊!哈哈

里面包含大量的自动配置类。

6. 总结一下

@SpringBootApplication等同于下面三个注解:

  • @SpringBootConfiguration
  • @EnableAutoConfiguration
  • @ComponentScan

其中@EnableAutoConfiguration是关键(启用自动配置),内部实际上就去加载META-INF/spring.factories文件的信息,然后筛选出以EnableAutoConfiguration为key的数据,加载到IOC容器中,实现自动配置功能!

四、开始自定义自己的starter

SpringBoot自动化配置源码分析从源码的角度讲解了 SpringBoot 自动化配置的原理,知道了它最终要干的事情不过是读取 META-INF/spring.factories 中的自动化配置类而已。

我们定义一个maven项目, 定义groupIdartifactId

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.test.starter</groupId>
    <artifactId>test-spring-boot-starter</artifactId>
    <version>1.0.0</version>
    <name>test-spring-boot-starter</name>
    <description>Demo project for Spring Boot</description>
</project>
1. 首先添加依赖

主要需要下面两个依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
    <version>2.1.8.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <version>2.1.8.RELEASE</version>
    <optional>true</optional>
</dependency>
<!-- 这个不是, 哈哈 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.8</version>
    <optional>true</optional>
</dependency>
2. 在resources中添加一个文件

resources文件夹中创建一个META-INF/spring.factories文件。

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.dtsz.sdk.config.SdkAutoConfiguration

com.dtsz.sdk.config.SdkAutoConfiguration这个是我们的自动配置类。

3. 编写自动化配置类

创建一个配置类,用来创建starter的配置信息

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @author long
 */
@Setter
@Getter
@ConfigurationProperties(prefix = "sso.test")
public class SsoProperties {

    /**
     * 登录的拦截路径
     */
    private String loginUrl;

    /**
     * 设置session 的过期时间
     */
    private int sessionMaxTime = 1800;

    /**
     * 默认的错误页面地址
     */
    private String errorPage = "/error/500";

}

这个文件将会读取application.yml文件中sso.test配置信息。


SdkAutoConfiguration自动配置类

import com.dtsz.sdk.factory.SsoProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author 墨龙吟
 * @version 1.0.0
 * @ClassName OssSdkAutoConfiguration.java
 * @Email [email protected]
 * @Description TODO   自动配置类
 * @createTime 2019年09月29日 - 11:02
 */
// 注解对着上面的表格查看含义
@Configuration
@ConditionalOnClass({TokenFactoryBean.class, SpringUtil.class, SendUtils.class})
@EnableConfigurationProperties(SsoProperties.class)
public class SdkAutoConfiguration {

    private Logger log = LoggerFactory.getLogger(SdkAutoConfiguration.class);

    @Autowired
    private SsoProperties ssoProperties;

    /**
     * 访问拦截器
     * @return
     */
    @Bean
    @ConditionalOnMissingBean(VisitFilter.class)
    public VisitFilter getVisitFilter() {
        return new VisitFilter();
    }
	
    /**
     * 将访问拦截器注入到spring容器
     */ 
    @Bean
    @ConditionalOnMissingBean(FilterRegistrationBean.class)
    public FilterRegistrationBean visitFilter() {
        log.info("注册访问拦截器");
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(getVisitFilter());
       filterRegistrationBean.addUrlPatterns(ssoProperties.getInterceptUrl().split(","));
        return filterRegistrationBean;
    }

}

VisitFilter拦截器

/**
 * 访问拦截器
 * @author long
 */
public class VisitFilter implements Filter {

    private Logger log = LoggerFactory.getLogger(VisitFilter.class);

    @Autowired
    private SsoProperties ssoProperties;

    /**
     * 初始化访问拦截器
     * @param filterConfig
     */
    @Override
    public void init(FilterConfig filterConfig) {
        log.info("初始化客户端访问拦截器");
        log.info(ssoProperties.toString());
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 处理逻辑
    }

    @Override
    public void destroy() {

    }

    private void doAuth(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 处理逻辑
    }

}
4. 使用自定义的starter

引入starter依赖 (官方starter会遵循spring-boot-starter-xxxx, 自定义的starter遵循xxxx-spring-boot-starter

<dependency>
    <groupId>com.test.starter</groupId>
    <artifactId>test-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

application.yml配置类的信息

# 配置项
sso:
  test:
    login-sys: /api/pageSys
    login-url: /api/page

controller中添加如下代码:

@Controller
@RequestMapping("/home")
public class HomeController {

    @Autowired
    private SendUtils sendComponent;

    @Autowired
    private SsoProperties ssoProperties;

    @RequestMapping("/getAll")
    @ResponseBody
    public String getData(HttpServletRequest request) throws IOException {
        String url = ssoProperties.getServerBaseUrl() + "/role/roleAll";
        String resultStr = sendComponent.request(url, null, request);
        return resultStr;
    }

}

项目启动后:

5. 注意点

不要在引入自定义starter依赖的项目中,操作starter中的bean, 会报bean不存在。

@Configuration
public class WebConfig {

    @Autowired
    private SsoProperties ssoProperties;

    /**
     * 访问拦截器
     * @return
     */
    @Bean
    public VisitFilter getVisitFilter() {
        return new VisitFilter();
    }

    @Bean
    @Order(value = 101)
    public FilterRegistrationBean visitFilter() {
        System.out.println("注册访问拦截器");
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(getVisitFilter());
        filterRegistrationBean.addUrlPatterns(ssoProperties.getInterceptUrl().split(","));
        return filterRegistrationBean;
    }
}

在引入自定义starter依赖的项目中,定义Configuration配置类,操作starter中的配置类,通过Autowired自动注入SsoPropertiesVisitFilter,会在项目启动的时候报SsoPropertiesBean没有被发现。我感觉可能是主项目的配置类的执行顺序可能靠前,starter加载的顺序靠后,所以会报错。日后验证吧!

五、参考资料

https://segmentfault.com/a/1190000018011535
https://fangjian0423.github.io/2016/11/16/springboot-custom-starter/
https://docs.spring.io/spring-boot/docs/2.2.0.BUILD-SNAPSHOT/reference/html/using-spring-boot.html#using-boot-structuring-your-code

六、关注一波公众号吧,感谢

Alt

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章