Evgeny
EvgenyBorisov Borisov(NAYA Technologies)和Kirill
Tolkkv Tolkachev(Cyan.Finance,
Twitter )以一个假想的铁库启动器为例,讨论了Spring Boot最重要,最有趣的时刻。

本文基于2017年Joker会议
上 Eugene和Cyril的
报告,其中部分是
报告的视频和文字记录。
Joker会议是由许多银行赞助的,因此,让我们想象一下,用于研究Spring Boot和我们创建的启动程序的应用程序已与银行连接。

因此,假设从Braavos的Iron Bank收到了一份订单申请书。 普通银行只是来回转账。 例如,像这样(我们有一个API):
http://localhost:8080/credit\?name\=Targarian\&amount\=100
在Iron Bank中,在转移资金之前,有必要由该银行的API计算是否有人可以退还该款项。 也许他在冬天无法生存,也没有人可以返回。 因此,提供了一种检查可靠性的服务。
例如,如果我们尝试将资金转移到塔加里,则该操作将被批准:

但是如果是Stark,则不会:

难怪:史塔克斯死的太频繁了。 如果一个人不能过冬,为什么要转账呢?
让我们看看它的内部外观。
@RestController @RequiredArgsConstructor public class IronBankController { private final TransferMoneyService transferMoney; private final MoneyDao moneyDao; @GetMapping("/credit") public String credit(@RequestParam String name, @RequestParam long amount) { long resultedDeposit = transferMoney.transfer(name, amount); if (resultedDeposit == -1) { return "Rejected<br/>" + name + " <b>will`t</b> survive this winter"; } return format( "<i>Credit approved for %s</i> <br/>Current bank balance: <b>%s</b>", name, resultedDeposit ); } @GetMapping("/state") public long currentState() { return moneyDao.findAll().get(0).getTotalAmount(); } }
这是一个常规的字符串控制器。
谁负责选择的逻辑,谁向谁发放贷款,谁对谁负责? 简单的说:如果您叫斯塔克,我们当然不会背叛。 在其他情况下-多么幸运。 普通银行。
@Service public class NameBasedProphetService implements ProphetService { @Override public boolean willSurvive(String name) { return !name.contains("Stark") && ThreadLocalRandom.current().nextBoolean(); } }
其他一切都不那么有趣。 这些注释为我们完成了所有工作。 一切都非常快。
所有主要配置在哪里? 只有一个控制器。 在Dao中,通常是一个空接口。
public interface MoneyDao extends JpaRepository<Bank, String> { }
在服务中-只能向您提供翻译和预测服务。 没有Conf目录。 实际上,我们只有application.yml(偿还债务者的列表)。 最主要的是main:
@SpringBootApplication @EnableConfigurationProperties(ProphetProperties.class) public class MoneyRavenApplication { public static void main(String[] args) { SpringApplication.run(MoneyRavenApplication.class, args); } }
那么所有的魔法都隐藏在哪里了?
事实是,开发人员不喜欢考虑依赖关系,配置配置(尤其是在这些配置是XML配置的情况下),而不是考虑其应用程序如何启动。 因此,Spring Boot为我们解决了这些问题。 我们只需要编写一个应用程序。
依存关系
我们始终遇到的第一个问题是版本冲突。 每次我们连接引用其他库的不同库时,就会出现依赖冲突。 每次我在Internet上阅读时,都需要添加一些实体管理器,然后出现一个问题,并且应该添加哪个版本,以便它不会破坏任何内容?
Spring Boot解决了版本冲突的问题。
我们通常如何获得一个Spring Boot项目(如果我们还没有来到某个已经存在的地方)?
- 或者转到start.spring.io ,放上乔什·朗(Josh Long)教我们设置的复选框,单击“下载项目”,然后打开已经存在所有内容的项目。
- 或使用IntelliJ,借助出现的选项,可以从此处直接设置Spring Initializer中的复选框。
如果我们使用Maven,则该项目将具有pom.xml,其中有一个称为
spring-boot-dependencies
的Spring Boot父
spring-boot-dependencies
。 将会有大量的依赖项管理。
我现在不再详细介绍Maven。 只需两个字。
依赖性管理块不注册依赖性。 这是一个块,可以在需要这些依赖项时指定版本。 而且,当您在依赖性管理块中指示某种依赖性而未指定版本时,Maven开始寻找是否有一个依赖性管理块,其中该版本的母体是在父pom或其他地方编写的。 即 在我的项目中,添加新的依赖项,我将不再指示版本,而是希望将其指示在父级中的某个位置。 而且,如果未在父级中指定它,则它当然不会与任何人产生任何冲突。 在我们的依赖关系管理中,指出了五百个很好的依赖关系,并且它们都相互一致。
但是有什么问题呢? 问题是,例如在我的公司中,我有自己的父母pom。 如果我想使用Spring,该如何处理我的父pom?

我们没有多重继承。 我们想使用我们的pom,并从外部获取依赖项管理块。

可以做到的。 注册依赖管理块的BOM导入就足够了。
<dependencyManagement> <dependencies> <dependency> <groupId>io.spring.platform</groupId> <artifactId>platform-bom</artifactId> <version>Brussels-SR2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
谁想进一步了解Bom,请参见报告“
Maven vs. Gradle ”。 所有这些都已详细解释。
如今,在大型公司中编写如此巨大的依赖管理块已变得非常流行,在这些块中,它们指示产品的所有版本以及使用其产品且彼此不冲突的产品的所有版本。 这就是所谓的宝。 无需继承即可将其导入到您的依赖关系管理块中。
这就是在Gradle中完成的方式(通常,同一件事,只是更简单):
dependencyManagement { imports { mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Dalston.RELEASE' } }
现在让我们谈谈依赖关系本身。
我们将在应用程序中编写什么? 依赖管理很好,但是我们希望应用程序具有某些功能,例如,通过HTTP响应,拥有数据库或对JPA的支持。 因此,我们现在所需要的只是获得三个依赖关系。
过去看起来像这样。 我想使用数据库,它开始了:需要某种事务管理器,因此需要spring-tx模块。 我需要一些休眠模式,因此需要EntityManager,休眠核心或其他功能。 我通过Spring进行所有配置,因此我需要Spring核心。 也就是说,对于一件简单的事情,您必须考虑十几个依赖项。
今天我们有了首发。 入门的想法是我们对它有依赖性。 首先,他汇总了他所来自的世界所需的依赖项。 例如,如果它是安全启动程序,那么您无需考虑需要什么依赖项,它们会以传递依赖项的形式立即到达启动程序。 或者,如果您正在使用Spring Data Jpa,则将依赖项放在启动器上,它将带来使用Spring Data Jpa所需的所有模块。
即 我们的pom如下所示:它仅包含我们需要的3-5个依赖项:
'org.springframework.boot:spring-boot-starter-web' 'org.springframework.boot:spring-boot-starter-data-jpa' 'com.h2database:h2'
通过解决依赖关系,一切都变得更加容易。 我们现在需要少考虑。 没有冲突,并且依赖项的数量减少了。
上下文设置
让我们谈谈我们一直面临的下一个痛苦-设置上下文。 每次我们从头开始编写应用程序时,都会花费大量时间来配置整个基础架构。 我们在xml或java config中注册了许多所谓的基础结构bean。 如果我们使用休眠模式,则需要EntityManagerFactory bean。 许多基础结构Bean-事务管理器,数据源等 -必须用手调整。 自然,它们全都落入上下文中。
在
Spring Ripper报告期间,我们在主环境中创建了上下文,如果它是xml上下文,则最初为空。 如果我们通过
AnnotationConfigApplicationContext
构建上下文,则有一些beanpostprocessors可以根据注释配置bean,但是上下文也几乎是空的。
现在主要是
SpringApplication.run
,没有可见的上下文:
@SpringBootApplilcation class App { public static void main(String[] args) { SpringApplication.run(App.class,args); } }
但是实际上我们有一个背景。
SpringApplication.run
我们返回一些上下文。

这是一个完全不典型的情况。 过去有两种选择:
- 如果这是一个桌面应用程序,则直接在主体中,您必须用手编写新的代码,选择
ClassPathXmlApplicationContext
,等等。
- 如果我们使用Tomcat,则有一个servlet管理器,按照某些约定,该管理器将查找XML,并默认情况下从中构建上下文。
换句话说,上下文是某种方式。 而且我们仍然将一些配置类传递给输入。 总的来说,我们选择了上下文类型。 现在我们只有
SpringApplication.run
,它将配置作为参数并构造一个上下文
谜语:我们可以通过那里吗?
鉴于:RipperApplication.class
public… main(String[] args) { SpringApplication.run(?,args); }
问题:还有什么可以转移到那里?
选项:RipperApplication.class
String.class
"context.xml"
new ClassPathResource("context.xml")
Package.getPackage("conference.spring.boot.ripper")
答案答案是:
文档说任何东西都可以在那里转移。 至少,它将编译并以某种方式起作用。

即 实际上,所有答案都是正确的。 它们中的任何一个都可以工作,甚至是
String.class
,在某些情况下,您甚至无需执行任何操作即可使其工作。 但这是一个不同的故事。
文档中唯一没有说的是以什么形式发送给我们的。 但这已经来自秘密知识领域。
SpringApplication.run(Object[] sources, String[] args) # APPLICATION SETTINGS (SpringApplication) spring.main.sources= # class name, package name, xml location spring.main.web-environment= # true/false spring.main.banner-mode=console # log/off
SpringApplication
在这里真的很重要-在幻灯片中,我们将与Carlson一起使用。
我们卡尔森根据传递给他的输入来创建某种上下文。 我想提醒您,例如,我们给他提供了五个
SpringApplication.run
选择,您可以使用
SpringApplication.run
使所有工作
SpringApplication.run
:
RipperApplication.class
String.class
"context.xml"
new ClassPathResource("context.xml")
Package.getPackage("conference.spring.boot.ripper")
SpringApplication
为我们做什么?

当我们通过
new
在main到
new
创建上下文时,我们有许多实现
ApplicationContext
接口的不同类:

卡尔森构建上下文时有哪些选择?
它仅产生两种上下文:Web上下文(
WebApplicationContext
)或通用上下文(
AnnotationConfigApplicationContext
)。

上下文的选择基于类路径中是否存在两个类:

即,配置数量没有减少。 要构建上下文,我们可以指定所有配置选项。 要构建上下文,我可以传递一个Groovy脚本或xml。 我可以指出要扫描或传递带有某些批注标记的类的软件包。 也就是说,我拥有所有的可能性。
但是,这是Spring Boot。 我们还没有创建单个bin,也没有创建单个类,我们只有main,并且在其中是我们的
SpringApplication.run
。 在入口处,他收到一个标有某种Spring Boot注释的类。
如果您考虑这种情况,那里会发生什么?
在我们的应用程序中,连接一对启动器后,共有436个垃圾箱。

差不多500个bean才开始写。

接下来,我们将了解这些bean的来源。
但首先,我们要这样做。
入门者的神奇之处在于,除了解决所有成瘾问题外,我们只连接了3-4个入门者,并且我们有436个垃圾箱。 我们将连接10个启动器,因此将有1000个以上的bin,因为每个启动器(除了依赖项之外)已经带来了配置,其中注册了一些必要的bin。 即 您说您想要Web的入门者,因此您需要一个servlet调度程序和
InternalResourceViewResolver
。 我们连接了jpa启动器-我们需要
EntityManagerFactory
bean。 所有这些bean已经在启动程序配置中某个位置,并且神奇地进入了应用程序,而我们没有采取任何行动。
为了理解它是如何工作的,今天我们将编写一个启动程序,它将启动基础结构容器到使用该启动程序的所有应用程序中。
铁法1.1。 总是发送乌鸦

让我们看一下客户的需求。 铁库有许多不同的应用程序在不同的分支机构中运行。 客户希望每次应用程序上升时都发送乌鸦-应用程序上升的信息。
让我们开始在特定铁库(Iron bank)的应用程序中编写代码。 我们将编写一个启动程序,以便所有依赖此启动程序的Iron Bank应用程序都可以自动发送乌鸦。 我们记得,启动器使我们能够自动加强依赖性。 最重要的是,我们几乎不编写任何配置。
我们创建一个监听器,监听要更新的上下文(最后一个事件),然后发送乌鸦。 我们将听
ContextRefreshEvent
。
public class IronListener implements ApplicationListener<ContextRefreshedEvent> { @Override public void onApplicationEvent(ContextRefreshedEvent event) { System.out.println(" ..."); } }
我们在启动程序配置中编写侦听器。 到目前为止,只会有一个侦听器,但是明天客户将要求其他一些基础结构组件,我们还将在此配置中编写它们。
@Configuration public class IronConfiguration { @Bean public RavenListener ravenListener() { return new RavenListener(); } }
出现了一个问题:如何使启动器的配置自动适合使用该启动器的所有应用程序?
在所有情况下,都有“启用某些功能”。

的确,如果我依靠20个入门者,则必须输入
@Enable
都有
@Enable
? 如果启动器有几种配置? 主要配置类将与
@Enable*
挂在一起,新年树怎么样?

实际上,我想在依赖级别获得某种控制反转。 我想连接启动器(以便一切正常),并且对它的内部名称一无所知。 因此,我们将使用spring.factories。

那么spring.factories
是spring.factories
该文档说,在spring.factories中,您需要指出接口的对应关系以及需要在它们上加载的内容-我们的配置。 所有这些将神奇地出现在上下文中,而各种条件将对其起作用。

因此,我们得到了所需的控制反转。

让我们尝试实现。 而不是访问我连接的启动器的内胆(采用此配置以及此...),一切将完全相反。 启动器将具有一个名为
spring.factories的文件。 在此文件中,我们规定应该为下载该启动器的每个人激活该启动器的配置。 再过一会儿,我将解释它在Spring Boot中的工作原理-从某个时候开始扫描所有jar,并寻找spring.factories文件。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.ironbank.IronConfiguration
@Configuration public class IronConfiguration { @Bean public RavenListener ravenListener() { return new RavenListener(); } }
现在剩下要做的就是连接项目中的启动器了。
compile project(':iron-starter')
在Maven中,类似-您需要注册依赖项。
我们启动我们的应用程序。 乌鸦应该在升起时起飞,尽管我们在应用程序本身中未做任何事情。 在基础架构方面,我们当然编写并配置了启动程序。 但是从开发人员的角度来看,我们只是连接了依赖关系,并且出现了配置-乌鸦飞了。 一切如我们所愿。
这不是魔术。 控制权的倒置应该不是魔术。 就像使用Spring应该不是魔术一样。 我们知道这是主要用于控制反转的框架。 由于您的代码存在控制反转,因此模块的控制也存在反转。
@SpringBootApplication周围的头
记住当我们用手建立上下文时的时刻。 我们编写了
new AnnotationConfigApplicationContext
并将一些配置传递给输入,它是一个Java类。 现在,我们还要编写
SpringApplication.run
并在其中传递类(即配置),只有它带有另一个功能非常强大的批注
@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 { … }
首先,内部有
@Configuration
,即它是一个配置。 您可以在
@Bean
编写
@Bean
然后照常注册Bean。
其次,
@ComponentScan
站在其上方。 默认情况下,它绝对扫描所有软件包和子软件包。 因此,如果您开始在同一程序包或
@Service
@RestController
@Service
@RestController
创建服务,则会自动扫描它们,因为主要配置会启动您的扫描过程。
实际上,
@SpringBootApplication
没有执行任何新操作。 他只是编译了Spring应用程序中的所有最佳实践,所以现在这是某种注释组合,包括
@ComponentScan
。
此外,还有
@EnableAutoConfiguration
之前没有的东西。 这是我在spring.factories中规定的班级。
@EnableAutoConfiguration
,如果您看,它带有
@EnableAutoConfiguration
:
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import({EnableAutoConfigurationImportSelector.class}) public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; Class<?>[] exclude() default {}; String[] excludeName() default {}; }
@EnableAutoConfiguration
的主要任务是在应用程序中进行我们想摆脱的导入,因为它的实现应该迫使我们从入门者那里写一些类的名称。 而且我们只能从文档中找到。 但是一切都应该靠自己。
您需要注意这一节课。 它以
ImportSelector
结尾。 在常规的Spring中,我们编写
Import(Some Configuration.class)
并像所有依赖项一样加载。 这是
ImportSelector
,这不是配置。
ImportSelector
我们所有的启动器
ImportSelector
入上下文。 它处理
@EnableAutoConfiguration
批注,该批注选择要加载的配置,并将在IronConfiguration中指定的bean添加到上下文中。

他是怎么做到的?
首先,它使用一个简单的实用程序类SpringFactoriesLoader,该类查看spring.factories并从所请求的内容中加载所有内容。 他有两种方法,但是没有太大区别。

Spring工厂加载程序存在于Spring 3.2中,只是没有人使用过。 显然,它被写为该框架的潜在发展。 因此,它发展成为Spring Boot,那里使用spring.factories约定的机制很多。 我们将进一步说明,除了配置之外,您还可以在spring.factories中编写-侦听器,异常处理器等。
static <T> List<T> loadFactories( Class<T> factoryClass, ClassLoader cl ) static List<String> loadFactoryNames( Class<?> factoryClass, ClassLoader cl )
这就是控制反转的工作方式。
我们似乎遵守开放封闭原则,因此不必每次都更改某些内容。每个启动器都为项目带来了很多有用的东西(到目前为止,我们只是在谈论它所携带的配置)。每个启动器都可以有自己的名为spring.factories的文件。在他的帮助下,他告诉自己所携带的物品。在Spring Boot中,有许多不同的机制可以使所有初学者了解spring.factories的内容。但是在整个方案中有一个细微差别。如果我们研究它在Spring本身中的工作方式,就像提出了整个org.springframework.boot:spring-boot-autoconfigure
入门方案的人们所写的那样,我们将看到他们有一个依赖项,META-INF / spring.factories与EnableAutoConfiguration
,并且有很多配置(我上次查看时,那里大约有80个未连接的自动配置)。 spring-boot-autoconfigure.jar/spring.factories</b> org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\ org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\ org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\ org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration.\ ...
也就是说,无论我连接启动器还是未连接启动器,当我使用Spring Boot时,总会有一个jar-s(Spring Boot本身的jar)中有一个其个人spring.factories,其中写入了90种配置。这些配置中的每一个都可以包含许多其他配置,例如CacheAutoConfiguration
,仅包含这样的东西-我们想要摆脱的东西: for (int i = 0; i < types.length; i++) { Imports[i] = CacheConfigurations.getConfigurationClass(types[i]); } return imports;
而且,然后从那里的类中静态地提取出一些映射,并且已加载的配置(不在spring.factory中)在此映射中进行了硬编码。他们不会那么容易找到。 private static final Map<CacheType, Class<?>> MAPPINGS; static { Map<CacheType, Class<?>> mappings = new HashMap<CacheType, Class<?>>(); mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class); mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class); mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class); mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class); mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class); mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class); mappings.put(CacheType.REDIS, RedisCacheConfiguration.class); mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class); addGuavaMapping(mappings); mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class); mappings.put(CacheType.NONE, NoOpCacheConfiguration.class); MAPPINGS = Collections.unmodifiableMap(mappings); }
最有趣的是,它们在引导阶段都会真正尝试引导。
他们会尝试的。但是:
总结中期结果。部分配置-良好,友善,正确的启动器,它们观察到控制权的反转和开放闭合原理-带有其spring.factory,它们的内胆被写入其中。我们将这样做;原则上,我们不能这样做。此外,Spring Boot本身还规定了配置的另一部分,这些部分总是被加载-其中有90多个。还有30种配置在Spring Boot中只是简单地硬编码。整个事情浮出水面,然后开始过滤配置。 2013年底有一份报告关于Spring 4的新功能,据说出现了一个注释@Conditional
,这使得可以在其注释中编写条件,这些条件引用返回true
或的类false
。依赖于此,是否创建了bean。由于Spring中的java配置也是bean,因此您也可以在其中设置不同的条件。因此,可以考虑配置,但是如果条件返回false
,则将丢弃它们。但是有细微差别。首先,根据某些环境设置,这可能导致垃圾箱可能存在或可能不会存在的情况。
以这个为例。铁法1.2。乌鸦只在生产中
客户有新要求。乌鸦是一件昂贵的事情,没有很多。因此,只有在我们知道产量增加的情况下才需要启动它们。
因此,只有在生产乌鸦的情况下,才能创建启动乌鸦的侦听器。让我们尝试去做。我们进入配置并编写: @Configuration <b>@ConditionalOnProduction</b> public class IronConfiguration { @Bean public RavenListener ravenListener() { return new RavenListener(); } }
我们如何决定是否生产?我有一家奇怪的公司,他说:“如果计算机上装有Windows,则表示不生产,而如果不是Windows,则表示生产。” 每个人都有自己的条件。钢铁银行特别指出,他们想手动进行管理:服务启动时,将弹出一个弹出窗口:“生产与否”。Spring Boot没有提供这样的条件。 @Retention(RUNTIME) @Conditional(OnProductionCondition.class) public @interface ConditionalOnProduction { }
我们做一个很好的老popap: public class OnProductionCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { return JOptionPane.showConfirmDialog(parentComponent: null, " ?") == 0; } }
让我们尝试一下。
我们提出服务,在窗口中单击“是”,然后乌鸦飞起来(创建了一个侦听器)。我们重新开始,回答不,乌鸦不飞。因此,注释@Conditional(OnProductionCondition.class)
指的是刚刚编写的类,这里有一个应返回true
或的方法false
。可以独立发明这种空调,这使应用程序非常动态,可以在不同条件下以不同方式工作。帕兹勒
所以@ConditionalOnProduction
我们写了。我们可以进行几种配置,使它们处于条件状态。假设我们有自己的条件,并且很受欢迎@ConditionalOnProduction
。例如,仅在生产中就需要15个豆。我用这个注释标记了他们。问题:找出是否是生产的逻辑,应该计算出多少次?它有什么区别?好吧,也许这种逻辑很昂贵,需要时间,而时间就是金钱。作为示例,我们提出了一个示例: @Configuration @ConditionalOn public class UndeadArmyConfiguration { ... } @Configuration public class DragonIslandConfiguration { @Bean @ConditionalOn public DragonGlassFactory dragonGlassFactory() { return new DragonGlassFactory(); } ... }
这里我们有两个垃圾箱:一个是常规的,一种是配置。两者都标有条件注释-仅在冬天来临时才需要。注释花费两次。在《权力的游戏》世界中,每次致电气象中心都非常昂贵-您每次都要付费以了解天气情况。
如果将其与缓存一起使用,则逻辑将仅被调用一次(即,OnProductionCondition.class
它将被调用一次,带有选择的窗口将只出现一次-是否生产)。一致的工作看起来合乎逻辑。另一方面,在某个时间点创建了一个配置,并且当某些更改发生时,可以在几秒钟内创建另一个bean。如果冬天在这5秒钟之内会怎样?正确答案不太清楚-300或400。实际上是某种完整的游戏。我们花了很长时间来初步了解正在发生的事情。它如何发生是一个单独的问题。情况就是这样。如果kondishn是一流上述(类@Component
,@Configuration
或@Service
连同他是kondishn),它满足每个这样的仓三次。此外,如果此配置已在启动器中注册,则需要两次。 @Configuration @ConditionalOn public class UndeadArmyConfiguration { ... }
如果bin在配置中注册,则总是一次。 @Configuration public class DragonIslandConfiguration { @Bean @ConditionalOn public DragonGlassFactory dragonGlassFactory() { return new DragonGlassFactory(); } ... }
因此,这个谜语没有确切答案,因为您需要找出配置的写入位置。如果它已在启动程序中注册,则由于某种原因其条件将起作用两次,无论如何,bin的条件将起作用一次,我们得到300。但是,如果配置不在Starter中,则仅其条件将启动3次,再加上一次该bin的时间。 。我们得到400。问题出现了:它甚至如何工作,为什么会这样?我的回答仅仅是:
它的运作方式无关紧要。重要的是要了解以下内容:编写条件注释时,值得自己在其中进行缓存,并通过静态字段进行缓存,这样就不会多次调用该逻辑。因为即使您一次使用了此注释,该逻辑也将不止一次地工作。铁法1.3。乌鸦在
我们将继续开发入门产品。我们必须以某种方式指定乌鸦的飞行。
我们在哪个文件中规定了初学者的东西?入门者带来了其中包含bean的配置。这些bean如何配置?他们从哪里获得数据源,用户等。当然,它们在所有情况下都具有默认值,但是如何允许重新定义默认值?有两个选项:application.properties
和application.yml
。您可以在此处输入一些信息,这些信息仍然可以在IDEA中自动完成。是什么让我们的入门者变得更糟?使用它的任何人还应该能够分辨出乌鸦飞向哪个地址-我们需要列出收件人列表。这是第一。其次,如果该人未注册收件人,我们希望不创建监听者,不要发送乌鸦。我们需要一个附加条件来创建乌鸦发送的侦听器。即
起动器本身是必需的,因为它除了乌鸦以外还可以有许多其他东西。但是,如果没有写出乌鸦应该飞到的地方,那根本就不会被创造出来。第三个-我们也想自动完成,以便那些将我们的入门者拉向自己的人对入门者读取的所有属性表示赞赏。对于每个任务,我们都有自己的工具。但是首先,您需要查看现有的注释。也许有什么适合我们的? @ConditionalOnBean @ConditionalOnClass @ConditionalOnCloudPlatform @ConditionalOnExpression @ConditionalOnJava @ConditionalOnJndi @ConditionalOnMissingBean @ConditionalOnMissingClass @ConditionalOnNotWebApplication @ConditionalOnProperty @ConditionalOnResource @ConditionalOnSingleCandidate @ConditionalOnWebApplication ...
确实,这里有些事情会帮助我们。首先@ConditionalOnProperty
。如果存在一个特定的属性或在application.yml中指定了某些值的属性,则此条件有效。同样,我们必须@ConfigurationalProperty
进行自动填充。自动完成
我们必须确保所有属性开始自动完成。如果这不仅会自动完成将在他们的application.yml中注册他们的人中,而且还会在我们的入门者中自动完成,那将是很好的。让我们将属性称为“乌鸦”。他必须知道在哪里飞。 @ConfigurationProperties("") public class RavenProperties { List<String> ; }
IDEA告诉我们这里有问题:
文档说我们没有添加依赖项(在Maven中将没有对文档的引用,而是一个按钮“添加依赖项”)。只需将其添加到您的项目中即可。 subproject { dependencies { compileOnly 'org.springframework.boot:spring-boot-configuration-processor' compile 'org.springframework.boot: spring-boot-starter' } }
现在,根据IDEA,我们拥有了一切。我将说明我们添加了哪种上瘾。每个人都知道什么是注释处理器。以简化的形式,这是可以在编译阶段做些事情的事情。例如,Lombok有其自己的注释处理器,该处理器在编译阶段会生成很多有用的代码-设置器,获取器。应用程序属性中的autocomplet属性从何而来?IDEA可以使用一个JSON文件。该文件描述了IDEA应该能够自动编译的所有属性。如果您想为入门者提供所需的属性,IDEA也可以自动编译,您有两种方法:- 您可以手动手动进入此JSON并以某种格式添加它们;
- 您可以从Spring Boot上拉注释处理器,它可以在编译阶段自行生成此JSON。神奇的Spring Boot注释决定了应该在其中添加哪些属性,我们可以使用这些注释来标记属于属性持有者的类。在编译阶段,注释处理器Spring Boot查找所有已标记的类
@ConfigurationalProperties
,从中读取name属性,并生成JSON。结果,每个依赖入门者的人都会收到此JSON作为礼物。
您还需要记住@EnableConfigurationProperties
,该类在您的上下文中以bean形式出现。 @Configuration @EnableConfigurationProperties(RavenProperties.class) public class IronConfiguration { @Bean @ConditionalOnProduction public RavenListener ravenListener() { return new RavenListener(); } }
一切看起来都不太好,但是您需要这样做,以便它比其余的Bean早一点出现(因为其余的Bean使用其属性进行自我配置)。结果,有必要添加两个注释:@EnableConfigurationProperties
通过指出谁的财产;
@ConfigurationalProperties
告诉一个前缀。
而且一定不能忘记吸气剂和吸气剂。它们也很重要,否则将无济于事-行动不会成功。结果,我们有了一个原则上可以手动编写的文件。但是没有人喜欢手动书写。 { "hints": [], "groups": [ { "sourceType": "com.ironbank.RavenProperties", "name": "", "type": "com.ironbankRavenProperties" } ], "properties": [ { "sourceType": "com.ironbank.RavenProperties", "name": ".", "type": "java.util.List<java.lang.String>" } ] }
乌鸦的地址
我们完成了任务的第一部分-我们获得了一些属性。但是还没有人与这些属性有关。现在,需要将它们设置为创建我们的侦听器的条件。 @Configuration @EnableConfigurationProperties(RavenProperties.class) public class IronConfiguration { @Bean @ConditionalOnProduction @ConditionalOnProperty(".") public RavenListener ravenListener() { return new RavenListener(); } }
我们又增加了一个条件-乌鸦只应在有人告诉要飞的地方的条件下才能制造。现在,我们将在application.yml中写到哪里。 spring: application.name: money-raven jpa.hibernate.ddl-auto: validate ironbank: ---: - : -: , : true
逻辑上仍然规定他飞到被告知的地方。为此,我们可以生成一个构造函数。新的Spring具有构造函数注入-这是推荐的方法。Eugene喜欢@Autowired
通过反射使所有内容出现在应用程序中。我喜欢遵循Spring提供的约定: @RequiredArgsConstructor public class RavenListener implements ApplicationListener<ContextRefreshedEvent>{ private final RavenProperties ravenProperties; @Override public void onApplicationEvent(ContextRefreshedEvent event) { ravenProperties.get().forEach(s -> { System.out.println(" … " + s); }); } }
但这不是免费的。一方面,您得到可验证的行为,另一方面,您得到一些痔疮。 @Configuration @EnableConfigurationProperties(RavenProperties.class) public class IronConfiguration { @Bean @ConditionalOnProduction @ConditionalOnProperty(".") public RavenListener ravenListener(RavenProperties r) { return new RavenListener(r); } }
没有任何地方@Aurowired
,使用Spring 4.3不能安装它。如果只有一个构造函数,则为@Aurowired
。在这种情况下,将使用批注,该批注@RequiredArgsConstructor
将生成单个构造函数。这等效于以下行为: public class RavenListener implements ApplicationListener<ContextRefreshedEvent>{ private final RavenProperties ravenProperties; public RavenListener(RavenProperties ravenProperties) { this.ravenProperties = ravenProperties; } public void onApplicationEvent(ContextRefreshedEvent event) { ravenProperties.get().forEach(s -> { System.out.println(" … " + s); }); } }
Spring建议以这种方式编写或使用Lombok。自2002年以来一直在编写Spring代码的80%的Jurgen Holler建议您对其进行设置@Aurowired
,以使其可见(否则,大多数人不会看到注入)。 public class RavenListener implements ApplicationListener<ContextRefreshedEvent>{ private final RavenProperties ravenProperties; @Aurowired public RavenListener(RavenProperties ravenProperties) { this.ravenProperties = ravenProperties; } public void onApplicationEvent(ContextRefreshedEvent event) { ravenProperties.get().forEach(s -> { System.out.println(" … " + s); }); } }
我们如何为这种方法付费?我们必须添加RavenProperties
到Java配置。如果我把它@Aurowired
放在田野上,则无需更改任何内容。因此,乌鸦被送出。我们完成了任务,这使我们的入门用户可以在其配置中获得称赞,同时我们获得了根据这些配置打开和关闭的垃圾箱。铁法1.4。风俗乌鸦
碰巧您需要自定义启动程序的行为。例如,我们有自己的黑乌鸦。我们需要一支能吸烟的白色烟,我们希望将其发送出去,以便人们可以看到地平线上有烟。让我们从寓言转向现实生活。入门者给我带来了一堆基础架构bean,这很棒。但是我不喜欢它们的配置方式。我进入了应用程序属性,并在那里进行了更改,现在我喜欢一切。但是在某些情况下,设置是如此复杂,以至于您自己注册数据源要比尝试确定应用程序属性容易。也就是说,我们希望自己将数据源注册在从启动器接收的bin中。那会发生什么呢?我自己注册了一些东西,而入门者给我带来了我的数据源。我现在有两个吗?还是会粉碎一个(哪一个呢?)我们想向您展示另一个条件,即只有在使用入门者的人没有此类垃圾箱的情况下,它才允许入门者携带某种垃圾箱。事实证明,这是完全不平凡的。我们已经提出了许多条件: @ConditionalOnBean @ConditionalOnClass @ConditionalOnCloudPlatform @ConditionalOnExpression @ConditionalOnJava @ConditionalOnJndi @ConditionalOnMissingBean @ConditionalOnMissingClass @ConditionalOnNotWebApplication @ConditionalOnProperty @ConditionalOnResource @ConditionalOnSingleCandidate @ConditionalOnWebApplication ...
原则上@ConditionalOnMissingBean
也有,所以只需使用现成的。让我们进入配置,在该配置中,我们指示仅当以前没有人创建过这样的bin时才应该创建它。 @Configuration @EnableConfigurationProperties(RavenProperties.class) public class IronConfiguration { @Bean @ConditionalOnProduction @ConditionalOnProperty(".") @ConditionalOnMissingBean</b> public RavenListener ravenListener(RavenProperties r) { return new RavenListener(r); } }
如果您打开大多数启动器,将会看到每个bin,每个配置都带有这样的注释包。我们只是想做一个模拟。尝试发射乌鸦时,它没有走,但事件出现了,我们在新的监听器-中编写了该事件MyRavenListener
。
这里有两个要点。第一点是,我们已经从现有的侦听器着迷了,并且没有在其中编写任何侦听器: @Component public class MyRavenListener implements ApplicationListener { public MyRavenListener(RavenProperties ravenProperties) { super(ravenProperties); } @Override public void onApplicationEvent(ContextRefreshedEvent event) { ravenProperties.get().forEach(s -> { System.out.println("event = " + event); }); } }
其次-我们在组件的帮助下做到了。如果我们采用Java配置,即 将注册与配置Bean相同的类,对我们没有任何帮助。如果我清理extends
并仅使某种类型的应用程序侦听器,它将@ConditionalOnMissingBean
无法正常工作。但是因为
该类也被称为,当我们尝试创建它时,我们可以编写ravenListener
-就像我们在配置中一样。上面,我们集中讨论了Java配置中的bean名称将由方法的名称组成的事实。在这种情况下,我们创建一个名为的bin ravenListener
。为什么您需要了解所有这一切?使用Spring Boot,一切通常都是超级的,但仅仅是在开始时。当项目前进时,出现一个启动器,第二个,第三个。您开始用手写的东西,因为即使是最好的入门者也无法提供您所需要的东西。垃圾桶冲突开始了。因此,如果您至少对如何确保未创建一个Bean以及如何在家中注册该Bean以使启动程序不会带来冲突(或者您有两个启动程序同时带来一个相同的启动程序)有一个大致的了解,那将是很好的。相同的垃圾箱,以免彼此冲突)。为了解决冲突,我正在编写我的bean,它将确保既不创建第一个也不创建第二个。而且,bean冲突是一个好情况,因为您看到了它。如果我们指定相同的bin名称,则不会有冲突。一个bean只会覆盖另一个bean。而且您将很长一段时间了解其中的内容。例如,如果我们制作某种数据源@Bean
,它将覆盖现有的数据源@Bean
。顺便说一句,如果入门者携带了您不需要的物品,只需制造一个具有相同ID的垃圾桶即可。是的,如果某个版本的启动程序更改了方法的名称,仅此而已,那么您的容器将再次为2。有条件的益智游戏
我们有@ConditionalOnClass
,@ConditionalOnMissingBean
有可能是作文课。例如,考虑执行配置。有肥皂,有绳子-我们挂在绞架上。有椅子和水流-将人放在椅子上是合乎逻辑的。断头台和心情愉快-这意味着您需要砍头。 @Configuration public class { @Bean @ConditionalOnClass({.class, .class}) @ConditionalOnMissingBean({.class}) public () { return new ("..."); } @Bean @ConditionalOnClass({.class, .class}) @ConditionalOnMissingBean({.class}) public c() { return new (" "); } @Bean @ConditionalOnClass({.class, .class}) @ConditionalOnMissingBean({.class}) public () { return new (" "); } }
我们将如何执行?问题:注释通常如何@ConditionalOnMissingClass
工作?假设我有一个将创建绞刑架的方法。但是,只有在有肥皂和绳索的情况下,才应建立绞刑架。但是没有肥皂。我怎么能知道没有肥皂或只有绳子。如果我尝试从方法中读取注释,而这些注释引用的不是此类,会发生什么?我可以接受这样的注释吗?答案选项:- ClassDefNotFound? , . - , ClassDefNotFound , reflection- , conditional as long as;
- , . reflection . , .
- ;
- .
: , . reflection . exception, , — . reflection? , , , , — ClassDefNotFound
.
这将使用ASM起作用。通过反射可以看到-什么都没有,Spring将有条件地手动解析字节码。他阅读该文件以免过早下载此文件,并了解@Conditional
肥皂,绳子的存在。他已经可以在上下文中单独检查这些类的存在。但是正如他们所说,ASM与速度无关。这是阅读类而不加载它并了解方法信息的机会。但是,尽管有一个注释OnMissingClass
可以将类名(字符串)作为参数,但Juergen Hoeller还是建议不要将其与类名绑定,而要编写条件语句。如果您遵循此建议,则一切运行都将更快,并且不需要ASM。但是根据消息来源判断,没有人这样做。铁法1.5。打开和关闭乌鸦
我们需要另一个属性-手动启用或禁用乌鸦的能力。为了不给任何人保证。这是我们向您展示的最后一个条件。除了乌鸦,我们的入门者什么也没给。因此,您可能会问,为什么能够打开/关闭它,您能否不接受它?但是在第二部分中,其他有用的东西将被填充到该启动器中。具体来说,可能不需要乌鸦-价格昂贵,可以将其关闭。同时,删除发送目的地不是很好-看起来像拐杖。因此,我们将通过完成一切@ConditionalOnProperty(".")
。 @Configuration @EnableConfigurationProperties(RavenProperties.class) public class IronConfiguration { @Bean @ConditionalOnProduction @ConditionalOnProperty(".") @ConditionalOnProperty(".") public RavenListener ravenListener(RavenProperties r) { return new RavenListener(r); } }
他向我们发誓这不可能完成:重复注释。问题是,如果我们具有带有某些参数的注释,则它是不可重复的。我们不能在两个属性上执行此操作。我们有用于注释的方法,有String,这是一个数组-您可以在其中指定多个属性。 @Conditional(OnPropertyCondition.class) public @interface ConditionalOnProperty { String[] value() default {}; String prefix() default ""; String[] name() default {}; String havingValue() default ""; boolean matchIfMissing() default false; boolean relaxedNames() default true; }
一切都很好,直到您尝试为该数组中的每个元素分别自定义值。我们有一个属性
,应该是false
并且是其他一些属性,应该string
具有一定的值。但是您只能在所有属性上指定一个值。也就是说,您不能这样做: @ConditionalOnProduction @ConditionalOnProperty(name = ".", havingValue="true") @ConditionalOnProperty(name = ".", havingValue="true") @ConditionalOnProperty(name = ".",havingValue="false") public IronBankApplicationListener applicationListener() { ... }
@ConditionalOnProduction @ConditionalOnProperty( name = { ".", ".", "." }, havingValue = "true" ) public IronBankApplicationListener applicationListener() { ... }
这里突出显示的不是数组。有一个变态允许您使用多个属性,但是它们只有一个值:AllNestedConditions
和AnyNestedCondition
。
坦率地说,它看起来很奇怪。但这确实有效。让我们尝试进行配置-一种同时考虑.
和的新条件.
。 @Configuration @EnableConfigurationProperties(RavenProperties.class) public class IronConfiguration { @Bean @ConditionalOnProduction @ConditionalOnProperty(".") @ConditionalOnRaven public RavenListener ravenListener(RavenProperties r) { return new RavenListener(r); } }
我们添加了注释@Conditional()
,在这里我们必须注册一些类。 @Retention(RUNTIME) @Conditional({OnRavenCondional.class}) public @interface CondionalOnRaven { }
我们创建它。 public class OnRavenCondional implements Condition { }
此外,我们必须实现某种条件,但由于具有以下注释,因此我们可能不这样做: public class CompositeCondition extends AllNestedConditions { @ConditionalOnProperty( name = ".", havingValue = "false") public static class OnRavenProperty { } @ConditionalOnProperty( name = ".enabled", havingValue = "true", matchIfMissing = true) public static class OnRavenEnabled { } ... }
我们有一个复合类Conditional
,它也将从另一个类- AllNestedConditions
或AnyNestedCondition
- 继承,它将包含其他类,这些类包含带有调味品的常用注释。即
相反,@Condition
我们必须指定: public class OnRavenCondional extends AllNestedConditions { public OnRavenCondional() { super(ConfigurationPhase.REGISTER_BEAN); } }
在这种情况下,您需要在内部创建一个构造函数。现在我们必须在这里创建静态类。我们进行某种类的学习(我们称其为R)。 public class OnRavenCondional extends AllNestedConditions { public OnRavenCondional() { super(ConfigurationPhase.REGISTER_BEAN); } public static class R {} }
我们实现我们的价值
(必须准确无误true
)。 public class OnRavenCondional extends AllNestedConditions { public OnRavenCondional() { super(ConfigurationPhase.REGISTER_BEAN); } @ConditionalOnProperty(".") public static class R {} @ConditionalOnProperty(value= ".", havingValue = "true") public static class C {} }
要重复此操作,只需记住类名。Spring具有良好的Java码头。您可以离开IDEA,阅读Java扩展坞并了解需要完成的工作。我们设置我们的@ConditionalOnRaven
。原则上,您可以同时包装@ConditionalOnProduction
,,和@ConditionalOnMissingBean
,但是现在我们将不这样做。看看发生了什么。 @Configuration @EnableConfigurationProperties(RavenProperties.class) public class IronConfiguration { @Bean @ConditionalOnProduction @ConditionalOnRaven @ConditionalOnMissingBean public RavenListener ravenListener(RavenProperties r) { return new RavenListener(r); } }
在没有
乌鸦的情况下不应飞翔。他没有飞。我不想打赌
,因为我们必须首先进行自动完成-这是我们的要求之一。 @Data @ConfigurationalProperties("") public class RavenProperties { List<String> ; boolean ; }
仅此而已。
默认情况下false
,设置true
到application.yml: jpa.hibernate.ddl-auto: validate ironbank: ---: - : : , : true
我们发射,我们的乌鸦飞了。因此,我们可以制作复合注释,并在其中添加任意数量的现有注释,即使它们不可重复。这适用于任何Java。它将拯救我们。在接下来的几天中将发布的文章的第二部分中,我们将重点介绍启动该应用程序的概要和精妙之处。
分钟的广告。
2018年Joker会议将于10月19日至20日举行,届时Evgeny Borisov将与Baruch Sadogursky一起做题为“软件开发世界中的Senor Holmes和Junior Watson历险记[Joker Edition]”,而Kirill Tolkachev和Maxim Gorelikov将发表报告“ Micronaut vs Spring Boot”。还是这里最小的是谁?” 。 总的来说,Joker将会有更多有趣且值得注意的报道。 门票可以
在会议的
官方网站上购买。
我们也为您做一个小调查!