SpringBoot学习笔记之starter
介绍
spring boot 应用 在配置上要比 spring应用简单很多的原因,就是在于 spring-boot-starter,stater 简单来说就是引入了一些相关依赖和一些初始化配置。
Spring 官方提供了很多starter,第三方也可以自定义starter,为了区分他们,starter默认以下命名规范。
- Spring官方:spring-boot-starter-xxx
- 第三方:xxx-spring-boot-starter
原理
SpringBoot 项目之所以能够帮助我们简化开发,主要是基于它提供的起步依赖和自动配置
起步依赖
就是将具备某种功能的坐标打包到一起,可以简化依赖导入的过程。
举个例子,我们如引入了 spring-boot-starter-web
这个坐标,那么会自动将 web 开发相关的 jar包一起导入到我们项目,例如 tomcat 相关包,可以提供内嵌的 tomcat 服务器。
自动配置
就是无需手动配置xml,自动配置并管理bean,简化开发过程。
SpringBoot 完成自动配置的关键步骤:
- 基于 Java 代码的Bean配置(注解配置)
- 自动配置条件依赖
- Bean 参数获取
- Bean 的发现
- Bean 的加载
接下来我们通过一个实际的例子mybatis-spring-boot-starter
来说明自动配置的实现过程。
基于java代码的Bean配置
当我们在项目中引入了这个坐标后,可以发现引入了很多mybatis相关的jar,其中有一个 mybatis-spring-boot-autoconfigur jar,我们主要分析这个jar包,打开这个jar包,里面有一个 **MybatisAutoConfiguration **自动配置类我们来大概分析一下这个类的源码。
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties({MybatisProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})
public class MybatisAutoConfiguration implements InitializingBean {
...
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
...
return factory.getObject();
}
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
}
...
}
上面截取了源码的主要地方
- @Configuration,用于定义配置类,被该注解修饰的类相当于是一个可以生产让Spring IOC容器管理的Bean的工厂。
- @Bean,该注解修饰的方法,可以将该方法返回的对象注册到Spring容器中。
所以 MybatisAutoConfiguration 配置类的一个重要作用就是:自动创建 SqlSessionFactory 和 SqlSessionTemplate 实例(需要 spring容器中有 DataSource 对象),并将它们交给spring容器进行管理。
下面是一些常用的条件依赖注解:
注解 | 说明 |
---|---|
@ConditionalOnBean | 仅在当前上下文中存在某个bean时,才会实例化这个Bean |
@ConditionalOnClass | 某个class位于类路径上,才会实例化这个Bean |
@ConditionalOnExpression | 当表达式为true的时候,才会实例化这个Bean |
@ConditionalOnMissingBean | 仅在当前上下文中不存在某个bean时,才会实例化这个Bean |
@ConditionalOnMissingClass | 某个class在类路径上不存在的时候,才会实例化这个Bean |
@ConditionalOnNotWebApplication | 不是web应用时才会实例化这个Bean |
@AutoConfigureAfter | 在某个bean完成自动配置后实例化这个bean |
@AutoConfigureBefore | 在某个bean完成自动配置前实例化这个bean |
Bean参数获取
要完成mybatis的自动配置,还需要提供数据源对象,而数据源对象的创建需要我们在配置文件中提供相关的配置参数,如数据库驱动、url,username等等,springboot的配置文件是application.properties或者application.yml,那么是如何从这个配置文件中读取相关属性进行数据源对象创建呢?
从上图 image1 中可以看到,mybatis-spring-boot-starter
这个坐标导入后会传递过来一个 spring-boot-autoconfigure
包,在这个包内有一个自动配置类 DataSourceAutoConfiguration
,其源码如下
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@ConditionalOnMissingBean(
type = {"io.r2dbc.spi.ConnectionFactory"}
)
@EnableConfigurationProperties({DataSourceProperties.class})
@Import({DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class})
public class DataSourceAutoConfiguration {
...
}
可以看到 @EnableConfigurationProperties({DataSourceProperties.class})
注解,打开 DataSourceProperties
类继续观察
@ConfigurationProperties(
prefix = "spring.datasource"
)
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
}
可以看到这个类上有 @ConfigurationProperties
注解,这个注解就是将yml或properties配置文件中的配置参数信息封装到ConfigurationProperties注解标注的Bean的相应属性上,而@EnableConfigurationProperties
注解的作用就是让 @ConfigurationProperties
注解生效。
Bean的发现
springboot 的引导类上有一个 @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 {};
@AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
@AliasFor(annotation = Configuration.class)
boolean proxyBeanMethods() default true;
}
- @SpringBootConfiguration:就相当于一个@Configuration注解,可以点进去看看,作用就是将被注解的类标注成一个bean配置类。
- @ComponentScan:自动扫描并加载符合条件的组件,并将这个组件交给spring容器管理。
- @EnableAutoConfiguration:这个注解很重要,借助@Import的支持,收集和注册依赖包中相关bean定义。
继续分析 @EnableAutoConfiguration 的源码:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}
- @AutoConfigurationPackage:将该注解的类所在的package作为自动配置package进行管理。
- @Import:导入需要自动配置的组件。此处为 AutoConfigurationImportSelector 类。
继续查看 AutoConfigurationImportSelector 源码,其中有一个 getCandidateConfigurations 方法
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;
}
这个方法中调用了SpringFactoriesLoader类的loadFactoryNames方法,继续追踪源码,可以发现SpringFactoriesLoader类的loadFactoryNames方法调用了本身的loadSpringFactories方法:
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();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
int var10 = var9.length;
for(int var11 = 0; var11 < var10; ++var11) {
String factoryImplementationName = var9[var11];
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
} catch (IOException var13) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
}
}
}
即 这个方法将从所有的jar包中读取 META-INF/spring.factories文件,而自动配置的类就是在这个文件中进行配置的:
这样 springboot 就可以加载到MybatisAutoConfiguration 这个配置类了。
Bean的加载
SpringBoot应用中,将一个普通类交给spring容器进行管理,有以下三种方式
- 使用 @Configuration与@Bean 注解
- 使用@Controller @Service @Repository @Component 注解标注该类并且启用@ComponentScan自动扫描
- 使用@Import 方法
springboot 实现自动配置就是使用的第三种方式(@Import),AutoConfigurationImportSelector类的selectImports方法返回一组从META-INF/spring.factories文件中读取的bean的全类名,这样Spring Boot就可以加载到这些Bean并完成实例的创建工作。
总结
- 使用 @Configuration 与 @Bean:基于Java代码的bean配置
- @Conditional:设置自动配置条件依赖
- @EnableConfigurationProperties与@ConfigurationProperties:读取配置文件转换为bean
- @EnableAutoConfiguration与@Import:实现bean发现与加载
自定义starter
案例源码 Gitee: https://gitee.com/me_muci/hello-spring-boot-starter.git
案例一
-
创建springboot项目 hello-spring-boot-starter
-
创建service包下 HelloService ,功能实现类,用于输入一句话
public class HelloService { private String name; private String address; public HelloService(String name, String address) { this.name = name; this.address = address; } public String sayHello(){ return "你好!我的名字叫 " + name + ",我来自 " + address; } }
-
创建config包下 HelloProperties 类,将配置文件中的属性,转化成bean
@ConfigurationProperties(prefix = "hello") public class HelloProperties { private String name; private String address; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } @Override public String toString() { return "HelloProperties{" + "name='" + name + '\'' + ", address='" + address + '\'' + '}'; } }
-
创建 config 包下 HelloServiceAutoConfiguration 类,该类有两个作用,一是配置类,可以被springboot发现,并注入HelloService实例 ,二是使 HelloProperties 类的@ConfigurationProperties 注解生效。
@Configuration @EnableConfigurationProperties(HelloProperties.class) public class HelloServiceAutoConfiguration { private HelloProperties helloProperties; /** * 通过构造方法注入配置属性对象HelloProperties * @param helloProperties */ public HelloServiceAutoConfiguration(HelloProperties helloProperties) { this.helloProperties = helloProperties; } /** * 实例化HelloService并载入Spring IoC容器 */ @Bean @ConditionalOnMissingBean public HelloService helloService() { return new HelloService(helloProperties.getName(), helloProperties.getAddress()); } }
-
创建 resource 目录下 META-INF 目录以及 spring.factories 文件,这个文件就是帮助springboot项目指明加载starter中的哪个配置类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ pers.muci.hellospringbootstarter.config.HelloServiceAutoConfiguration
-
将该 starter 安装到本地Maven仓库,可以使用 Maven 插件 install
-
使用 这个 starter
-
创建 一个新的 springboot 项目,勾选 web 选项
<dependencies> <!--刚才自定义的 starter--> <dependency> <groupId>pers.muci</groupId> <artifactId>hello-spring-boot-starter</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies>
-
修改 application.yml
server: port: 8080 hello: name: muci address: hefei
-
创建controller 包下面的 HelloController
@RestController @RequestMapping("/hello") public class HelloController { @Autowired HelloService helloService; @GetMapping("/say") public String sayHello(){ return helloService.sayHello(); } }
-
启动项目,访问 localhost:8080/hello/say
-
案例二
在上面的基础上,自定义一个 MyLog 拦截器。
主要功能就是提供一个在方法上使用的注解 @MyLog(desc = “sayHello方法”) , 当方法被调用时,可以在控制台打印方法执行的时间、方法名等
该案例在 案例一的基础上进行开发。
-
新增pom文件依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> </dependency>
-
创建 log 包下的 MyLog 注解。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyLog { /** * 方法描述 * @return */ String desc() default ""; }
-
log 包下的 MyLogInterceptor
public class MyLogInterceptor extends HandlerInterceptorAdapter { private static final ThreadLocal<Long> startTimeThreadLocal = new ThreadLocal<>(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); MyLog myLog = method.getAnnotation(MyLog.class); if (myLog != null){ long startTime = System.currentTimeMillis(); startTimeThreadLocal.set(startTime); } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); MyLog myLog = method.getAnnotation(MyLog.class); if (myLog != null){ long endTime = System.currentTimeMillis(); Long startTime = startTimeThreadLocal.get(); long optTime = endTime - startTime; String requestUri = request.getRequestURI(); String methodName = method.getDeclaringClass().getName() + "." + method.getName(); String methodDesc = myLog.desc(); System.out.println("请求uri:" + requestUri); System.out.println("请求方法名:" + methodName); System.out.println("方法描述:" + methodDesc); System.out.println("方法执行时间:" + optTime + "ms"); } } }
-
创建 config 包下 MyLogAutoConfiguration
@Configuration public class MyLogAutoConfiguration implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new MyLogInterceptor()); } }
-
修改 META-INF 包下 spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ pers.muci.hellospringbootstarter.config.HelloServiceAutoConfiguration,\ pers.muci.hellospringbootstarter.config.MyLogAutoConfiguration
-
使用Maven插件安装到本地仓库
-
使用starter
还是使用 案例一中创建的springboot项目,修改 HelloController ,在say方法上使用 @MyLog 注解
@RestController @RequestMapping("/hello") public class HelloController { @Autowired HelloService helloService; @MyLog(desc = "sayHello方法") @GetMapping("/say") public String sayHello(){ return helloService.sayHello(); } }
启动项目,访问 localhost:8080/hello/say ,查看控制台输出。
ngbootstarter.config.MyLogAutoConfiguration -
使用Maven插件安装到本地仓库
-
使用starter
还是使用 案例一中创建的springboot项目,修改 HelloController ,在say方法上使用 @MyLog 注解
@RestController @RequestMapping("/hello") public class HelloController { @Autowired HelloService helloService; @MyLog(desc = "sayHello方法") @GetMapping("/say") public String sayHello(){ return helloService.sayHello(); } }
启动项目,访问 localhost:8080/hello/say ,查看控制台输出。