一、引言
SpringBoot是基于Spring基础上而生的一个微服务框架,相比于Spring,强调约定大于配置,具有自动化配置、快速开发,自动部署等优点。那么SpringBoot是怎么来简化原先Spring那些繁琐的配置的呢?自动装配。本篇博客,主要是想简单阐述一下SpringBoot的自动装配机制和手写自定义一个starter。
二、自动装配原理
2.1 从熟知的启动类开始
@SpringBootApplication public class Start { public static void main(String[] args) {
// 启动IOC和tomcat容器 SpringApplication.run(Start.class, args); } }
这个是我们熟悉的SpringBoot的启动类。自动装配的核心就是注解@SpringBootApplication。
2.2 注解@SpringBootApplication
@SpringBootApplication是SpringBoot自定义的一个注解。我们先来看这个注解的源码片段,实际上对自动装配产生作用的就是两个:
@SpringBootConfiguration 和 @EnableAutoConfiguration
@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 {
}
先简单分析源码的前四个注解。这个四个注解属于Java的元注解,意为修饰注解的注解。
@Target:自定义注解的使用范围,如类、方法、属性。比方说@Controller是作用在类上的,如果作用在方法上就会报错。
@Retention:这个注解有三个属性:source、class、runtime(最常见也最常用),主要定义自定义注解的生效时间。
(1)source,意味着当Java文件编译成源文件(.class),这个注解就会被遗弃,只保留在源文件,主要是提供给编译用的,比方说我们熟悉的Override;
(2)class,意味着实现一些检查性的操作,如supperessWarning,摒弃一些告警标识;
(3)runtime:意味着运行时生效,最常见和使用。
@Documented:生成javadoc文档
@Inherited:所修饰的自定义注解可以被子类继承。如我们刚才看到的Start类,如果有一个类继承了它,那么默认也会拥有@SpringBootApplication注解的功能。
2.3 @SpringBootConfiguration
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration(proxyBeanMethods = false) public @interface SpringBootConfiguration { /** * Specify whether {@link Bean @Bean} methods should get proxied in order to enforce * bean lifecycle behavior, e.g. to return shared singleton bean instances even in * case of direct {@code @Bean} method calls in user code. This feature requires * method interception, implemented through a runtime-generated CGLIB subclass which * comes with limitations such as the configuration class and its methods not being * allowed to declare {@code final}. * <p> * The default is {@code true}, allowing for 'inter-bean references' within the * configuration class as well as for external calls to this configuration's * {@code @Bean} methods, e.g. from another configuration class. If this is not needed * since each of this particular configuration's {@code @Bean} methods is * self-contained and designed as a plain factory method for container use, switch * this flag to {@code false} in order to avoid CGLIB subclass processing. * <p> * Turning off bean method interception effectively processes {@code @Bean} methods * individually like when declared on non-{@code @Configuration} classes, a.k.a. * "@Bean Lite Mode" (see {@link Bean @Bean's javadoc}). It is therefore behaviorally * equivalent to removing the {@code @Configuration} stereotype. * @return whether to proxy {@code @Bean} methods * @since 2.2 */ @AliasFor(annotation = Configuration.class) boolean proxyBeanMethods() default true; }
这个注解只有一个属性proxyBeanMethods,而且这个属性是从Configuration继承而来的。默认为true,它会采用CGLIB去做代理这个配置类。
抛开上面三个元注解,就只有一个@Configuration。也就是说@SpringBootApplication可以等同于@Configuration注解。
扩展:@Configuration和@Component区别
2.4 @EnableAutoConfiguration
自动装配主要依赖@EnableAutoConfiguration来实现
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import(AutoConfigurationImportSelector.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 {}; }
2.4.1 @Import注解解释
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Import { /** * {@link Configuration @Configuration}, {@link ImportSelector}, * {@link ImportBeanDefinitionRegistrar}, or regular component classes to import. */ Class<?>[] value(); }
由注解的解释,我们可以看到,@Import作为Spring的注解,主要是想将类注入到IOC中去,主要由以下三种用法:
(1)实现ImportSelector接口selectImport方法。这个方法返回的是类的全路径的数据,这个数组中的类都会被扫描到IOC容器。
public interface ImportSelector { /** * Select and return the names of which class(es) should be imported based on * the {@link AnnotationMetadata} of the importing @{@link Configuration} class. */ String[] selectImports(AnnotationMetadata importingClassMetadata); }
(2)实现ImportBeanDefinitionRegistrar接口。注意看参数,BeanDefinitionRegistry registry,是通过registry将bean注入到IOC容器。这是一个手工注入bean,自己编码去实现。
public interface ImportBeanDefinitionRegistrar { /** * Register bean definitions as necessary based on the given annotation metadata of * the importing {@code @Configuration} class. * <p>Note that {@link BeanDefinitionRegistryPostProcessor} types may <em>not</em> be * registered here, due to lifecycle constraints related to {@code @Configuration} * class processing. * <p>The default implementation delegates to * {@link #registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry)}. * @param importingClassMetadata annotation metadata of the importing class * @param registry current bean definition registry * @param importBeanNameGenerator the bean name generator strategy for imported beans: * {@link ConfigurationClassPostProcessor#IMPORT_BEAN_NAME_GENERATOR} by default, or a * user-provided one if {@link ConfigurationClassPostProcessor#setBeanNameGenerator} * has been set. In the latter case, the passed-in strategy will be the same used for * component scanning in the containing application context (otherwise, the default * component-scan naming strategy is {@link AnnotationBeanNameGenerator#INSTANCE}). * @since 5.2 * @see ConfigurationClassPostProcessor#IMPORT_BEAN_NAME_GENERATOR * @see ConfigurationClassPostProcessor#setBeanNameGenerator */ default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) { registerBeanDefinitions(importingClassMetadata, registry); } /** * Register bean definitions as necessary based on the given annotation metadata of * the importing {@code @Configuration} class. * <p>Note that {@link BeanDefinitionRegistryPostProcessor} types may <em>not</em> be * registered here, due to lifecycle constraints related to {@code @Configuration} * class processing. * <p>The default implementation is empty. * @param importingClassMetadata annotation metadata of the importing class * @param registry current bean definition registry */ default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { } }
(3)普通类,直接被Spring扫描到IOC中去。
2.4.2 @AutoConfigurationPackage
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import(AutoConfigurationPackages.Registrar.class) public @interface AutoConfigurationPackage { }
我们来看:AutoConfigurationPackages.Registrar实现了ImportBeanDefinitionRegistrar就是想手动去注入bean。注册又是一个什么样的bean,又要做什么呢?
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports { @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)); } }
我们看源码的registerBeanDefinitions方法,new PackageImport(),这是一个内部类,首先它会先去获取AutoConfigurationPackage,最终要获得这个类上的basePackages,当然如果你覆写了basePackage这个属性就是你定义,没有就是我们当前Start类的当前类的路径,这个包路径不允许程序所修改,得到这个路径之后,封装成一个不可变的集合赋给全局的变量this.packageNames。
private static final class PackageImports { private final List<String> packageNames; PackageImports(AnnotationMetadata metadata) { AnnotationAttributes attributes = AnnotationAttributes .fromMap(metadata.getAnnotationAttributes(AutoConfigurationPackage.class.getName(), false)); List<String> packageNames = new ArrayList<>(); for (String basePackage : attributes.getStringArray("basePackages")) { packageNames.add(basePackage); } for (Class<?> basePackageClass : attributes.getClassArray("basePackageClasses")) { packageNames.add(basePackageClass.getPackage().getName()); } if (packageNames.isEmpty()) { packageNames.add(ClassUtils.getPackageName(metadata.getClassName())); } this.packageNames = Collections.unmodifiableList(packageNames); } List<String> getPackageNames() { return this.packageNames; } @Override public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } return this.packageNames.equals(((PackageImports) obj).packageNames); } @Override public int hashCode() { return this.packageNames.hashCode(); } @Override public String toString() { return "Package Imports " + this.packageNames; } }
this.packageNames这个全局变量作用是什么呢?回到registerBeanDefinitions()实现的register,这里的packageNames就是刚才得到的包路径,这里当然还有我们熟悉和Spring生命周期相关的的BeanDefinition,通过BasePackage类构造器,赋值给全局packages,提供get()方法给别人做查询使用。
public static void register(BeanDefinitionRegistry registry, String... packageNames) { if (registry.containsBeanDefinition(BEAN)) { BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN); ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues(); constructorArguments.addIndexedArgumentValue(0, addBasePackages(constructorArguments, packageNames)); } else { GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); beanDefinition.setBeanClass(BasePackages.class); beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, packageNames); beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); registry.registerBeanDefinition(BEAN, beanDefinition); } }
2.4.3 @Import(AutoConfigurationImportSelector.class)
我们注解上的这个类:AutoConfigurationImportSelector
@Override public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return NO_IMPORTS; } AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata); return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); }
实现的这个方法返回包含类的全路径的数组。
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } AnnotationAttributes attributes = getAttributes(annotationMetadata); List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); configurations = removeDuplicates(configurations); Set<String> exclusions = getExclusions(annotationMetadata, attributes); checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = getConfigurationClassFilter().filter(configurations); fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationEntry(configurations, exclusions); }
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; }
loadFactoryName主要是加载Bean的。getSpringFactoriesLoaderFactoryClass()返回地是EnableAutoConfiguration.class对象,getBeanClassLoader获得类加载器。这里使用到类加载器,主要就是想加载资源,加载什么样的资源,继续如下:
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) { String factoryTypeName = factoryType.getName(); return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList()); } private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { MultiValueMap<String, String> result = cache.get(classLoader); if (result != null) { return result; } 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 factoryTypeName = ((String) entry.getKey()).trim(); for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { result.add(factoryTypeName, factoryImplementationName.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"这个相对路径下的资源,我们找到这个资源:
这里面类似key-value的配置,value可以看作是一些具体的实现类,我们重点先关注下:org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,
这些配置的实现类最终都会加载到IOC容器中去。
在这个EnableAutoConfiguration中,就有一个:org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration。这里实现中,通过@Bean就会把我们熟悉的DispacherServlet加载到IOC容器中去。这里就是为什么使用SpringBoot时,就没让我们再去配置DispacherServlet了。
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass(DispatcherServlet.class) @AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class) public class DispatcherServletAutoConfiguration { /* * The bean name for a DispatcherServlet that will be mapped to the root URL "/" */ public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet"; @Configuration(proxyBeanMethods = false) @Conditional(DefaultDispatcherServletCondition.class) @ConditionalOnClass(ServletRegistration.class) @EnableConfigurationProperties(WebMvcProperties.class) protected static class DispatcherServletConfiguration { @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) { DispatcherServlet dispatcherServlet = new DispatcherServlet(); dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest()); dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest()); dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound()); dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents()); dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails()); return dispatcherServlet; }
当然,你可能问?光是加载到容器就足够了吗?因为以前可还需要将Servlet加载到tomcat容器的上下文啊,这样才能正常的运转啊?那么,这里SpringBoot是怎么做到的呢?
也是在同一个类,还有一个@Bean。看这个:DispatcherServletRegistrationBean。这个类的父类是:ServletRegistrationBean。可能你会比较熟悉,因为我们可能会利用它去开发和web三大组件相关的内容:Filter、Listener、Servlet。
@Configuration(proxyBeanMethods = false) @Conditional(DispatcherServletRegistrationCondition.class) @ConditionalOnClass(ServletRegistration.class) @EnableConfigurationProperties(WebMvcProperties.class) @Import(DispatcherServletConfiguration.class) protected static class DispatcherServletRegistrationConfiguration { @Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME) @ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) { DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath()); registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME); registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup()); multipartConfig.ifAvailable(registration::setMultipartConfig); return registration; } }
ServletRegistrationBean继承于RegistrationBean。RegistrationBean这个类有几个是是实现类。其中,可以看到ServletListenerResgistrationBean(监听器)等。
而在ServletRegistrationBean中,有这么一个方法:通过ServletContext的addServlet方法。将servlet添加到tomcat容器中去。这里的this.servlet是上面DispatcherServletRegistrationBean 通过构造器传入进来的dispacherServlet.
@Override protected ServletRegistration.Dynamic addRegistration(String description, ServletContext servletContext) { String name = getServletName(); return servletContext.addServlet(name, this.servlet); }
问题是:addRegistration这个是怎么触发的呢?再来看以下RegistrationBean。
public abstract class RegistrationBean implements ServletContextInitializer, Ordered
这个类实现了一个接口:ServletContextInitializer,利用了tomcat的SPI规范,将这个接口的实现类注入到tomcat的上下文中去,在tomcat的上下文,就会去调用onStartup方法。最终就会触发到addRegistration。
@FunctionalInterface public interface ServletContextInitializer { /** * Configure the given {@link ServletContext} with any servlets, filters, listeners * context-params and attributes necessary for initialization. * @param servletContext the {@code ServletContext} to initialize * @throws ServletException if any call against the given {@code ServletContext} * throws a {@code ServletException} */ void onStartup(ServletContext servletContext) throws ServletException; }
言归正传,再回到我们的AutoConfigurationImportSelector的getAutoConfigurationEntry,为了方便,这里再贴一下这个方法的代码:、
/** * Return the {@link AutoConfigurationEntry} based on the {@link AnnotationMetadata} * of the importing {@link Configuration @Configuration} class. * @param annotationMetadata the annotation metadata of the configuration class * @return the auto-configurations that should be imported */ protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } AnnotationAttributes attributes = getAttributes(annotationMetadata); List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); configurations = removeDuplicates(configurations); Set<String> exclusions = getExclusions(annotationMetadata, attributes); checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = getConfigurationClassFilter().filter(configurations); fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationEntry(configurations, exclusions); }
当扫描到一堆配置类之后,通过removeDuplicates方法做了一个去重,去重的目的就是因为在classpath下的spring.factories文件内容都要扫描,去除相同的配置。
getExclusions就是排除到定义的不需要自动装配的类。
checkExcludedClasses则是需要进行自动装配的类做一个检查,不合法的需要校验。
紧接着会加载一个过滤器再进行一遍过滤。这个过滤器就是AutoConfigurationImportFilter。
fireAutoConfigurationImportEvents相当于Springboot提供的一个扩展点,基于事件驱动的,如果程序想参与到这个自动装配的话,可以去监听这个事件,监听到这个事件之后,可以做自己的一些操作。相关AutoConfigurationImportListener
最后,将所有需要装配的类封装到一个对象上:AutoConfigurationEntry。最终转化作为selectImport的返回值。
三、自定义写一个starter
3.1 自动装配的使用场景
这个自动装配除了帮我们自动装配DispatcherServlet,帮我们去加载类似tomcat、redis等,那我们还可以用来做什么?比方说实际开发中的一些公共的jar包(公共组件)等;
举个例子,比方说我们去开发一个Filter,要把这个Filter加载到应用程序中去(RegistrationBean );或者想使用一个服务,@Autowired 去注入jar里面的一个bean,但是你直接注入是不行的,可能你需要先使用<bean/>标签或者@Bean的注解。因为我要把其他这个jar包的bean通过bean标签或者注解写入进来,就对我的代码有了侵入,因为假如这个jar包的名字改了,或者这么bean我不用了(就像刚才的filter),我除了剔除jar包,我还要修改很多依赖的代码。也就是这样公共包会显得比较重。
有了自动装配,比方说Filter,可以把filter的装配自闭在一个jar包里面,别人不想使用了,剔除相应jar就行。就像想使用@Autowired去注入别人bean,不需要自己写<bean/>或者@Bean。直接@Autowired去使用就好。
3.2 简单案例--自定义starter
先提及到一个开发规范,对于包命名问题。
- autoconfiguration:自动装配的核心代码。
- starter:管理Jar。如果是Spring官方的,spring-boot-starter-xxx, 如果是自己定义,命名xxx-spring-boot-starter。
3.2.1 使用spring.factories
首先,我们做一个简单获取当前时间的工具类和一个filter(filter就简单打印一个访问时间),放在util-spring-boot-autoconfigure
工程目录大概如下,boot-application依赖starter,starter依赖autoconfigure:
代码简单如下:
public class DateUtil { public String getNowTime() { LocalDateTime localDate = LocalDateTime.now(); return localDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss:SSS")); } }
public class MyFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { long start = System.currentTimeMillis(); HttpServletRequest request = (HttpServletRequest)servletRequest; filterChain.doFilter(servletRequest, servletResponse); long end = System.currentTimeMillis(); System.out.println(request.getRequestURI() + "执行时间:" + (end - start)); } }
自定义DateConfig,把前面这两个加载进来:
@Configuration public class DateConfig { @Bean public DateUtil getDateUtil() { return new DateUtil(); } @Bean public FilterRegistrationBean registerFilter() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setFilter(new MyFilter()); filterRegistrationBean.addUrlPatterns("/*"); filterRegistrationBean.setName("costFilter"); filterRegistrationBean.setOrder(1); return filterRegistrationBean; } }
最后在spring.factories中写入:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=util.spring.boot.autoconfigure.DateConfig
3.2.2 使用注解的方式
核心通过实现ImportSelector:
public class MyImport implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
return new String[]{DateConfig.class.getName()};
}
}
自定义注解:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Import(MyImport.class) public @interface EnableUtil { }
这个时候,可以不需要再spring.factories中配置,但是需要在启动类上增加修饰注解:
@SpringBootApplication @EnableUtil public class Start { public static void main(String[] args) { SpringApplication.run(Start.class, args); } }
3.2.3 直接Import
@SpringBootApplication @Import(DateConfig.class) public class Start { public static void main(String[] args) { SpringApplication.run(Start.class, args); } }
对比上面三种方式,第一种最好,因为实现了零侵入,可插拔。
本文涉及的demo测试代码可以参考gitee: https://gitee.com/leijisong/my-boot-starter