Evgeny
EvgenyBorisov Borisov (NAYA Technologies) y Kirill
tolkkv Tolkachev (Cyan.Finance,
Twitter ) hablan sobre los momentos más importantes e interesantes de Spring Boot en el ejemplo de un titular para un banco de hierro imaginario.

El artículo se basa en el
informe de Eugene y Cyril de nuestra conferencia Joker 2017. Debajo del corte está la transcripción del video y el texto del informe.
La conferencia Joker está patrocinada por muchos bancos, así que imaginemos que la aplicación en la que estudiaremos el trabajo de arranque de Spring y el iniciador que creamos está conectada con el banco.

Entonces, supongamos que se recibe una orden para una solicitud del Banco de Hierro de Braavos. Un banco ordinario simplemente transfiere dinero de un lado a otro. Por ejemplo, así (tenemos una API para esto):
http://localhost:8080/credit\?name\=Targarian\&amount\=100
Y en Iron Bank, antes de transferir dinero, es necesario que la API del banco calcule si una persona puede devolverlo. Tal vez no sobrevivirá al invierno y no habrá nadie a quien regresar. Por lo tanto, se proporciona un servicio que verifica la confiabilidad.
Por ejemplo, si intentamos transferir dinero a Targaryen, la operación será aprobada:

Pero si Stark, entonces no:

No es de extrañar: Starks muere con demasiada frecuencia. ¿Por qué transferir dinero si una persona no sobrevive al invierno?
Veamos cómo se ve por dentro.
@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(); } }
Este es un controlador de cadena normal.
¿Quién es responsable de la lógica de elección, a quién otorgar un préstamo y a quién no? Línea simple: si su nombre es Stark, ciertamente no traicionamos. En otros casos, qué suerte. Banco ordinario
@Service public class NameBasedProphetService implements ProphetService { @Override public boolean willSurvive(String name) { return !name.contains("Stark") && ThreadLocalRandom.current().nextBoolean(); } }
Todo lo demás no es tan interesante. Estas son algunas anotaciones que hacen todo el trabajo por nosotros. Todo es muy rapido.
¿Dónde están todas las configuraciones principales? Solo hay un controlador. En Dao, generalmente es una interfaz vacía.
public interface MoneyDao extends JpaRepository<Bank, String> { }
En servicios: solo servicios de traducción y predicción a los que puede enviar. No hay directorio Conf. De hecho, solo tenemos application.yml (una lista de quienes pagan deudas). Y principal es el más común:
@SpringBootApplication @EnableConfigurationProperties(ProphetProperties.class) public class MoneyRavenApplication { public static void main(String[] args) { SpringApplication.run(MoneyRavenApplication.class, args); } }
Entonces, ¿dónde está escondida toda la magia?
El hecho es que a los desarrolladores no les gusta pensar en dependencias, configurar configuraciones, especialmente si se trata de configuraciones XML, y pensar en cómo se inicia su aplicación. Por lo tanto, Spring Boot nos resuelve estos problemas. Solo necesitamos escribir una solicitud.
Dependencias
El primer problema que siempre tuvimos es un conflicto de versiones. Cada vez que conectamos diferentes bibliotecas que hacen referencia a otras bibliotecas, aparecen conflictos de dependencia. Cada vez que leo en Internet que necesito agregar algún administrador de entidades, surge una pregunta y ¿qué versión debo agregar para que no rompa nada?
Spring Boot resuelve el problema de los conflictos de versiones.
¿Cómo solemos obtener un proyecto Spring Boot (si no hemos llegado a algún lugar donde ya existe)?
- o vaya a start.spring.io , marque las casillas de verificación que Josh Long nos enseñó a configurar, haga clic en Descargar proyecto y abra el proyecto donde todo ya está allí;
- o use IntelliJ, donde, gracias a la opción aparecida, las casillas de verificación en Spring Initializer se pueden configurar desde allí.
Si trabajamos con Maven, entonces el proyecto tendrá pom.xml, donde hay un padre Spring Boot llamado
spring-boot-dependencies
. Habrá un gran bloque de gestión de dependencias.
No entraré en los detalles de Maven ahora. Solo dos palabras.
El bloque de gestión de dependencias no registra dependencias. Este es un bloque con el que puede especificar versiones en caso de que se necesiten estas dependencias. Y cuando indica algún tipo de dependencia en el bloque de administración de dependencias sin especificar la versión, Maven comienza a buscar si hay un bloque de administración de dependencias en el que la versión principal está escrita en el pom principal o en otro lugar. Es decir en mi proyecto, agregando una nueva dependencia, ya no indicaré la versión con la esperanza de que se indique en algún lugar del padre. Y si no se especifica en el padre, entonces ciertamente no creará ningún conflicto con nadie. En nuestra gestión de dependencias, se indican unas buenas quinientas dependencias, y todas son coherentes entre sí.
Pero cual es el problema? El problema es que en mi empresa, por ejemplo, tengo mi propio padre pom. Si quiero usar Spring, ¿qué debo hacer con mi padre pom?

No tenemos herencia múltiple. Queremos usar nuestro pom y obtener el bloque de gestión de dependencias desde el exterior.

Esto se puede hacer. Es suficiente registrar la importación BOM del bloque de gestión de dependencias.
<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>
¿Quién quiere saber más sobre bom? Vea el informe "
Maven vs. Gradle ". Allí todo esto se explicó en detalle.
Hoy en día, entre las grandes empresas se ha puesto de moda escribir bloques tan enormes de gestión de dependencias, donde indican todas las versiones de sus productos y todas las versiones de productos que usan sus productos y que no entran en conflicto entre sí. Y esto se llama bom. Esto puede importarse a su bloque de gestión de dependencias sin herencia.
Y así es como se hace en Gradle (como de costumbre, lo mismo, pero más fácil):
dependencyManagement { imports { mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Dalston.RELEASE' } }
Ahora hablemos de las dependencias mismas.
¿Qué escribiremos en la aplicación? La gestión de dependencias es buena, pero queremos que la aplicación tenga ciertas capacidades, por ejemplo, para responder a través de HTTP, tener una base de datos o soporte para JPA. Por lo tanto, todo lo que necesitamos ahora es obtener tres dependencias.
Solía verse así. Quiero trabajar con la base de datos y comienza: se necesita algún tipo de administrador de transacciones, por lo que se necesita el módulo spring-tx. Necesito un poco de hibernación, por lo que se requiere EntityManager, hibernate-core u otra cosa. Configuro todo a través de Spring, por lo que necesito Spring Core. Es decir, por una cosa simple, tenía que pensar en una docena de dependencias.
Hoy tenemos entrantes. La idea de un iniciador es que le ponemos una dependencia. Para empezar, agrega las dependencias que se necesitan para el mundo del que vino. Por ejemplo, si se trata de un iniciador de seguridad, entonces no piensa qué dependencias se necesitan, inmediatamente llegan en forma de dependencias transitivas al iniciador. O si está trabajando con Spring Data Jpa, ponga una dependencia en el iniciador y traerá todos los módulos necesarios para trabajar con Spring Data Jpa.
Es decir Nuestro pom se ve así: contiene solo esas 3-5 dependencias que necesitamos:
'org.springframework.boot:spring-boot-starter-web' 'org.springframework.boot:spring-boot-starter-data-jpa' 'com.h2database:h2'
Con las dependencias resueltas, todo se volvió más fácil. Necesitamos pensar menos ahora. No hay conflicto y el número de dependencias ha disminuido.
Configuración del contexto
Hablemos del siguiente dolor que siempre tuvimos: establecer el contexto. Cada vez que comenzamos a escribir una aplicación desde cero, lleva mucho tiempo configurar toda la infraestructura. Registramos en xml o java config muchos de los llamados beans de infraestructura. Si trabajamos con hibernate, necesitábamos el bean EntityManagerFactory. Una gran cantidad de beans de infraestructura: administrador de transacciones, fuente de datos, etc. - Era necesario ajustarse con las manos. Naturalmente, todos cayeron en contexto.
Durante el informe
Spring Ripper , creamos el contexto en el principal, y si era el contexto xml, inicialmente estaba vacío. Si creamos el contexto a través de
AnnotationConfigApplicationContext
, hay algunos procesadores de beanpost que podrían configurar los beans según las anotaciones, pero el contexto también estaba casi vacío.
Y ahora en main hay
SpringApplication.run
y no hay contexto visible:
@SpringBootApplilcation class App { public static void main(String[] args) { SpringApplication.run(App.class,args); } }
Pero en realidad tenemos un contexto.
SpringApplication.run
nos devuelve algo de contexto.

Este es un caso completamente atípico. Solía haber dos opciones:
- si se trata de una aplicación de escritorio, directamente en la página principal, tenía que escribir una nueva con las manos, seleccionar
ClassPathXmlApplicationContext
, etc.
- Si trabajamos con Tomcat, entonces había un administrador de servlets, que, según algunas convenciones, buscaba XML y, por defecto, creaba un contexto a partir de él.
En otras palabras, el contexto era de alguna manera. Y todavía pasamos algunas clases de configuración a la entrada. En general, elegimos el tipo de contexto. Ahora solo tenemos
SpringApplication.run
, toma configuraciones como argumentos y construye un contexto
Riddle: ¿qué podemos pasar allí?
Dado:RipperApplication.class
public… main(String[] args) { SpringApplication.run(?,args); }
Pregunta: ¿qué más se puede transferir allí?
Opciones:RipperApplication.class
String.class
"context.xml"
new ClassPathResource("context.xml")
Package.getPackage("conference.spring.boot.ripper")
La respuestaLa respuesta es:
La documentación dice que cualquier cosa puede transferirse allí. Como mínimo, esto se compilará y de alguna manera funcionará.

Es decir De hecho, todas las respuestas son correctas. Se puede hacer que cualquiera de ellos funcione, incluso
String.class
, y en algunas condiciones ni siquiera tiene que hacer nada para que funcione. Pero esta es una historia diferente.
Lo único que no se dice en la documentación es en qué forma enviarnos allí. Pero esto ya es del reino del conocimiento secreto.
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
es realmente importante aquí: más abajo en las diapositivas lo tendremos con Carlson.
Nuestro Carlson crea algún tipo de contexto basado en la información que le transmitimos. Le recuerdo, le damos, por ejemplo, cinco maravillosas opciones que puede hacer que todo funcione con
SpringApplication.run
:
RipperApplication.class
String.class
"context.xml"
new ClassPathResource("context.xml")
Package.getPackage("conference.spring.boot.ripper")
¿Qué hace
SpringApplication
por nosotros?

Cuando creamos el contexto a través de
new
en main a través de
new
, tuvimos muchas clases diferentes que implementan la interfaz
ApplicationContext
:

¿Y qué opciones hay cuando Carlson construye el contexto?
Solo tiene dos tipos de contexto: un contexto web (
WebApplicationContext
) o un contexto genérico (
AnnotationConfigApplicationContext
).

La elección del contexto se basa en la presencia de dos clases en el classpath:

Es decir, el número de configuraciones no ha disminuido. Para construir un contexto, podemos especificar todas las opciones de configuración. Para construir el contexto, puedo pasar un script maravilloso o xml; Puedo indicar qué paquetes escanear o pasar la clase marcada con algunas anotaciones. Es decir, tengo todas las posibilidades.
Sin embargo, este es Spring Boot. Todavía no hemos creado un único contenedor, ni una sola clase, solo tenemos main, y en él está nuestro Carlson -
SpringApplication.run
. En la entrada, recibe una clase marcada con algún tipo de anotación Spring Boot.
Si nos fijamos en este contexto, ¿qué pasará allí?
En nuestra aplicación, después de conectar un par de arrancadores, había 436 contenedores.

Casi 500 frijoles solo para comenzar a escribir.

A continuación, entenderemos de dónde provienen estos frijoles.
Pero antes que nada, queremos hacer lo mismo.
La magia de los principiantes, además de resolver todos los problemas con las adicciones, es que conectamos solo 3-4 iniciadores y tenemos 436 contenedores. Conectaríamos 10 arrancadores, habría más de 1000 bins, porque cada arrancador, excepto las dependencias, ya trae configuraciones en las que se registran algunos bins necesarios. Es decir Usted dijo que desea un iniciador para la web, por lo que necesita un despachador de servlets y
InternalResourceViewResolver
. Conectamos el iniciador jpa; necesitamos el bean
EntityManagerFactory
. Todos estos beans ya están en algún lugar de las configuraciones iniciales, y llegan mágicamente a la aplicación sin ninguna acción de nuestra parte.
Para comprender cómo funciona esto, hoy escribiremos un iniciador, que también traerá contenedores de infraestructura a todas las aplicaciones que usan este iniciador.
Ley del Hierro 1.1. Siempre envía un cuervo

Veamos el requisito del cliente. Iron Bank tiene muchas aplicaciones diferentes que se ejecutan en diferentes sucursales. Los clientes quieren que se envíe un cuervo cada vez que se eleva la aplicación, información que la aplicación ha aumentado.
Comencemos a escribir el código en la aplicación de un banco de hierro específico (banco de hierro). Escribiremos un iniciador para que todas las aplicaciones de Iron Bank que dependan de este iniciador puedan enviar automáticamente un cuervo. Recordamos que los iniciadores nos permiten ajustar automáticamente las dependencias. Y lo más importante, no escribimos casi ninguna configuración.
Creamos un oyente que escucha el contexto para actualizarse (último evento), después de lo cual envía un cuervo. Escucharemos
ContextRefreshEvent
.
public class IronListener implements ApplicationListener<ContextRefreshedEvent> { @Override public void onApplicationEvent(ContextRefreshedEvent event) { System.out.println(" ..."); } }
Escribimos oyente en la configuración inicial. Hasta ahora, solo habrá un oyente, pero mañana el cliente pedirá algunas otras piezas de infraestructura, y también las escribiremos en esta configuración.
@Configuration public class IronConfiguration { @Bean public RavenListener ravenListener() { return new RavenListener(); } }
Surge la pregunta: ¿cómo hacer que la configuración de nuestro iniciador se ajuste automáticamente a todas las aplicaciones que utilizan este iniciador?
Para todas las ocasiones hay un "habilitar algo".

Realmente, si dependo de 20 titulares, tendré que poner
@Enable
? ¿Y si el motor de arranque tiene varias configuraciones? La clase de configuración principal se colgará con
@Enable*
, ¿cómo es el árbol de Año Nuevo?

De hecho, quiero obtener algún tipo de inversión de control en el nivel de dependencia. Quiero conectar el motor de arranque (para que todo funcione), y no sé nada sobre cómo se llama su interior. Por lo tanto, usaremos spring.factories.

Entonces, ¿qué es la spring.factories
La documentación dice que hay tales spring.factories en las que debe indicar la correspondencia de las interfaces y lo que necesita cargar en ellas: nuestras configuraciones. Y todo esto aparecerá mágicamente en contexto, mientras que varias condiciones funcionarán en ellos.

Así obtenemos la inversión de control, que necesitábamos.

Tratemos de implementar. En lugar de acceder a las entrañas del motor de arranque que conecté (tome esta configuración, y esto ...), todo será exactamente lo contrario. El iniciador tendrá un archivo llamado
spring.factories . En este archivo, prescribimos qué configuración de este iniciador debe activarse para todos los que lo hayan descargado. Un poco más adelante explicaré cómo funciona exactamente esto en Spring Boot: en algún momento comienza a escanear todos los frascos y busca el archivo spring.factories.
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.ironbank.IronConfiguration
@Configuration public class IronConfiguration { @Bean public RavenListener ravenListener() { return new RavenListener(); } }
Ahora todo lo que nos queda por hacer es conectar el iniciador en el proyecto.
compile project(':iron-starter')
En Maven, de manera similar, debe registrar la dependencia.
Lanzamos nuestra aplicación. El cuervo debería despegar en el momento en que sube, aunque no hicimos nada en la aplicación en sí. En términos de infraestructura, nosotros, por supuesto, escribimos y configuramos el motor de arranque. Pero desde el punto de vista del desarrollador, solo conectamos la dependencia y apareció la configuración: el cuervo voló. Todo lo que quisimos.
Esto no es mágico. La inversión del control no debería ser mágica. Así como el uso de Spring no debería ser mágico. Sabemos que este es un marco principalmente para la inversión de control. Como hay una inversión de control para su código, también hay una inversión de control para los módulos.
@SpringBootApplication alrededor de la cabeza
Recuerde el momento en que construimos el contexto en general con nuestras manos. Escribimos un
new AnnotationConfigApplicationContext
y pasamos alguna configuración a la entrada, que era una clase java. Ahora también escribimos
SpringApplication.run
y pasamos la clase allí, que es la configuración, solo que está marcada con otra anotación bastante poderosa
@SpringBootApplication
, que lleva a todo el mundo.
@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 { … }
En primer lugar, dentro hay
@Configuration
, es decir, es una configuración. Puede escribir
@Bean
y, como de costumbre, registrar los beans.
En segundo lugar,
@ComponentScan
encuentra por encima de él. Por defecto, escanea absolutamente todos los paquetes y subpaquetes. En consecuencia, si comienza a crear servicios en el mismo paquete o en sus
@Service
(
@Service
,
@RestController
), se escanean automáticamente, ya que la configuración principal inicia su proceso de escaneo.
En realidad,
@SpringBootApplication
no hace nada nuevo. Simplemente compiló todas las mejores prácticas que estaban en las aplicaciones de Spring, por lo que ahora se trata de algún tipo de composición de anotaciones, incluido
@ComponentScan
.
Además, todavía hay cosas que no estaban allí antes:
@EnableAutoConfiguration
. Esta es la clase que prescribí en spring.factories.
@EnableAutoConfiguration
, si nos fijamos, lleva
@Import
con él:
@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 {}; }
La tarea principal de
@EnableAutoConfiguration
es hacer la importación de la que queríamos deshacernos en nuestra aplicación, porque su implementación debería habernos obligado a escribir el nombre de alguna clase desde el inicio. Y solo podemos averiguarlo a partir de la documentación. Pero todo debería ser por sí mismo.
Debes prestar atención a esta clase. Termina con
ImportSelector
. En Spring normal, escribimos
Import(Some Configuration.class)
alguna configuración y se carga, como todos sus dependientes. Esto es
ImportSelector
, esto no es una configuración.
ImportSelector
todos nuestros iniciadores en contexto. Procesa la anotación
@EnableAutoConfiguration
de spring.factories, que selecciona qué configuraciones cargar y agrega los beans que especificamos en IronConfiguration al contexto.

¿Cómo lo hace él?
En primer lugar, utiliza una clase de utilidad sencilla, SpringFactoriesLoader, que analiza spring.factories y carga todo lo que se le pide. Él tiene dos métodos, pero no son muy diferentes.

Spring Factories Loader existió en Spring 3.2, pero nadie lo usó. Aparentemente fue escrito como un desarrollo potencial del marco. Y así se convirtió en Spring Boot, donde hay tantos mecanismos que utilizan la convención spring.factories. Además, mostraremos que, además de la configuración, también puede escribir en spring.factories: oyentes, procesadores inusuales, etc.
static <T> List<T> loadFactories( Class<T> factoryClass, ClassLoader cl ) static List<String> loadFactoryNames( Class<?> factoryClass, ClassLoader cl )
Así es como funciona la inversión de control.
Parece que cumplimos con el principio abierto cerrado, de acuerdo con el cual no es necesario cambiar algo en algún lugar cada vez. Cada iniciador lleva muchas cosas útiles al proyecto (hasta ahora solo estamos hablando de las configuraciones que lleva). Y cada iniciador puede tener su propio archivo llamado spring.factories. Con su ayuda, le dice qué lleva exactamente. Y en Spring Boot hay muchos mecanismos diferentes que son capaces de todos los principiantes de aportar lo que dicen las fábricas de spring.Pero hay un matiz en todo este esquema. Si vamos a estudiar cómo funciona en Spring, como escriben las personas que han org.springframework.boot:spring-boot-autoconfigure
ideado todo este esquema de iniciadores, veremos que tienen una dependencia , hay una línea en META-INF / spring.EnableAutoConfiguration
, y tiene muchas configuraciones (la última vez que miré, había alrededor de 80 configuraciones automáticas desconectadas allí). 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.\ ...
Es decir, independientemente de si conecté el motor de arranque o no, cuando trabajo con Spring Boot, siempre habrá uno de los jar-s (jar del propio Spring Boot), en el que está su spring.factories personal, donde se escriben 90 configuraciones. Cada una de estas configuraciones puede contener muchas otras configuraciones, por ejemplo CacheAutoConfiguration
, que contienen tal cosa, algo de lo que queríamos escapar: for (int i = 0; i < types.length; i++) { Imports[i] = CacheConfigurations.getConfigurationClass(types[i]); } return imports;
Por otra parte, entonces algún mapa se saca estáticamente de la clase allí, y las configuraciones cargadas (que no están en esta primavera. Fábricas) están codificadas en este mapa. No serán tan fáciles de encontrar. 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); }
, .

. :

. — , , , open closed principle — spring.factories, . , - .
, Spring Boot, — 90 . 30 , Spring Boot.
, . 2013
sobre las novedades en Spring 4, donde se dijo que apareció una anotación @Conditional
, lo que hace posible escribir condiciones en sus anotaciones que se refieren a clases que regresan true
o false
. Dependiendo de esto, los beans se crean o no. Dado que la configuración de Java en Spring también es un bean, también puede establecer diferentes condiciones allí. Por lo tanto, se consideran las configuraciones, pero si se devuelve condicional false
, se descartarán.Pero hay matices. En primer lugar, esto lleva a una situación en la que el contenedor puede o no estar, dependiendo de algunas configuraciones del entorno.
Considera esto como un ejemplo.Ley de Hierro 1.2. Cuervo solo en producción
El cliente tiene un nuevo requisito. Raven es algo caro, no hay muchos. Por lo tanto, deben lanzarse solo si sabemos que la producción ha aumentado.
En consecuencia, el oyente que lanza el cuervo solo debe crearse si es producción. Intentemos hacerlo.Entramos en la configuración y escribimos: @Configuration <b>@ConditionalOnProduction</b> public class IronConfiguration { @Bean public RavenListener ravenListener() { return new RavenListener(); } }
¿Cómo decidimos si es producción o no? Tenía una compañía extraña que decía: "Si Windows está en la máquina, significa no producción, pero si no Windows, entonces producción". Cada uno tiene su propio condicional.Específicamente, el Banco de Hierro dijo que quieren administrar esto manualmente: cuando el servicio sube, debería aparecer una ventana emergente: "producción o no". Tal condición no se proporciona en Spring Boot. @Retention(RUNTIME) @Conditional(OnProductionCondition.class) public @interface ConditionalOnProduction { }
Hacemos un buen viejo popap: public class OnProductionCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { return JOptionPane.showConfirmDialog(parentComponent: null, " ?") == 0; } }
Probémoslo.
Elevamos el servicio, hacemos clic en Sí en la ventana y el cuervo vuela (se crea un oyente).Comenzamos de nuevo, responda No, el cuervo no vuela.Entonces, la anotación se @Conditional(OnProductionCondition.class)
refiere a la clase recién escrita, donde hay un método que debería devolver true
o false
. Tal aire acondicionado se puede inventar independientemente, lo que hace que la aplicación sea muy dinámica, le permite funcionar de manera diferente en diferentes condiciones.Pazzler
Entonces @ConditionalOnProduction
escribimos. Podemos hacer varias configuraciones, ponerlas a condición. Supongamos que tenemos nuestra propia condición y es popular, como @ConditionalOnProduction
. Y hay, por ejemplo, 15 frijoles que solo se necesitan en la producción. Los marqué con esta anotación.Pregunta: la lógica que descubre si es producción o no, ¿cuántas veces debería funcionar?¿Qué diferencia funciona? Bueno, tal vez esta lógica es cara, lleva tiempo y el tiempo es dinero.A modo de ilustración, se nos ocurrió un ejemplo: @Configuration @ConditionalOn public class UndeadArmyConfiguration { ... } @Configuration public class DragonIslandConfiguration { @Bean @ConditionalOn public DragonGlassFactory dragonGlassFactory() { return new DragonGlassFactory(); } ... }
: , . - — , .
. — , , .

, (
OnProductionCondition.class
, — ). . , , , - . 5 ?
no muy claro: 300 o 400. En realidad, hay algún tipo de juego completo. Estuvimos dando vueltas durante mucho tiempo para entender primero lo que estaba sucediendo. Cómo sucede es un tema aparte.La situación es así. Si kondishn está por encima de la primera clase (clase @Component
, @Configuration
o @Service
junto con él es kondishn), cumple tres veces en cada uno de tales bin. Además, si esta configuración está registrada en el iniciador, entonces dos veces. @Configuration @ConditionalOn public class UndeadArmyConfiguration { ... }
Si el contenedor está registrado dentro de la configuración, siempre una vez. @Configuration public class DragonIslandConfiguration { @Bean @ConditionalOn public DragonGlassFactory dragonGlassFactory() { return new DragonGlassFactory(); } ... }
, , . , - , , 300. , , . 400.
: ? :

, . : -, , , . , , .
1.3.
Continuamos desarrollando nuestro motor de arranque. De alguna manera debemos especificar el vuelo del cuervo.
¿En qué archivo prescribimos cosas para el principiante? Los principiantes traen una configuración en la que hay beans. ¿Cómo se configuran estos beans? ¿De dónde obtienen la fuente de datos, el usuario, etc. Naturalmente, tienen valores predeterminados para todas las ocasiones, pero ¿cómo permiten que esto se redefina? Hay dos opciones: application.properties
y application.yml
. Allí puede ingresar información que se completará maravillosamente en IDEA.¿Qué empeora nuestro arranque? Cualquiera que lo use también debería poder decir en qué direcciones vuela el cuervo: debemos hacer una lista de destinatarios. Este es el primero.En segundo lugar, queremos que el oyente no se cree y que el cuervo no se envíe si la persona no registró los destinatarios. Necesitamos una condición adicional para crear un oyente que envíe el cuervo. Es decir
el iniciador en sí mismo es necesario porque puede tener muchas cosas diferentes además de un cuervo. Pero si no está escrito dónde debe volar el cuervo, ese simplemente no se crea.Y el tercero: también queremos autocompletar, para que las personas que empujaron nuestro motor de arranque contra ellos mismos reciban un cumplido sobre todas las propiedades que lee el motor de arranque.Para cada una de estas tareas, tenemos nuestra propia herramienta. Pero antes que nada, debes mirar las anotaciones existentes. Tal vez algo nos convenga? @ConditionalOnBean @ConditionalOnClass @ConditionalOnCloudPlatform @ConditionalOnExpression @ConditionalOnJava @ConditionalOnJndi @ConditionalOnMissingBean @ConditionalOnMissingClass @ConditionalOnNotWebApplication @ConditionalOnProperty @ConditionalOnResource @ConditionalOnSingleCandidate @ConditionalOnWebApplication ...
De hecho, hay cosas aquí que nos ayudarán. En el primer lugar @ConditionalOnProperty
. Esta es una condición que funciona si hay una propiedad específica o propiedad con algún valor especificado en application.yml. Del mismo modo, tenemos @ConfigurationalProperty
que hacer un autocompletado.Autocompletar
Debemos asegurarnos de que todas las propiedades comiencen a autocompletarse. Sería bueno que esto se completara automáticamente no solo entre las personas que los registrarán en su application.yml, sino también en nuestro iniciador.Llamemos a nuestra propiedad "cuervo". Debe saber a dónde volar. @ConfigurationProperties("") public class RavenProperties { List<String> ; }
IDEA nos dice que algo está mal aquí:
la documentación dice que no hemos agregado una dependencia (en Maven no habría referencia a la documentación, sino un botón "agregar dependencia"). Solo agrégalo a tu proyecto. subproject { dependencies { compileOnly 'org.springframework.boot:spring-boot-configuration-processor' compile 'org.springframework.boot: spring-boot-starter' } }
Ahora, según IDEA, tenemos todo.Explicaré qué tipo de adicción agregamos. Todo el mundo sabe qué es el procesador de anotaciones. En una forma simplificada, esto es algo que puede hacer algo en la etapa de compilación. Por ejemplo, Lombok tiene su propio procesador de anotaciones, que genera una gran cantidad de código útil en la etapa de compilación: establecedores, captadores.¿De dónde viene el autocompletado en la propiedad que está en las propiedades de la aplicación? Hay un archivo JSON con el que IDEA puede trabajar. Este archivo describe todas las propiedades que IDEA debería poder compilar automáticamente. Si desea la propiedad que se le ocurrió para el iniciador, IDEA también puede compilar automáticamente, tiene dos formas:- puede ingresar manualmente a este JSON usted mismo y agregarlos en un formato determinado;
- puede extraer el procesador de anotaciones de Spring Boot, que puede generar esta pieza de JSON en la etapa de compilación. Las propiedades que se deben agregar allí están determinadas por la anotación mágica Spring Boot, con la que podemos marcar las clases que son propietarios. En la etapa de compilación, el procesador de anotación Spring Boot encuentra todas las clases que están etiquetadas
@ConfigurationalProperties
, lee la propiedad de nombre de ellas y genera JSON. Como resultado, todos los que dependerán del iniciador recibirán este JSON como regalo.
También debe recordar @EnableConfigurationProperties
que esta clase aparece dentro de su contexto como un bean. @Configuration @EnableConfigurationProperties(RavenProperties.class) public class IronConfiguration { @Bean @ConditionalOnProduction public RavenListener ravenListener() { return new RavenListener(); } }
Todo no se ve muy bien, pero debe hacer esto para que aparezca un poco antes que el resto de los beans (porque el resto de los beans utilizan su propiedad para configurarse).Como resultado, fue necesario poner dos anotaciones:@EnableConfigurationProperties
indicando las propiedades de quién;
@ConfigurationalProperties
diciendo qué prefijo.
Y no hay que olvidar a los captadores y setters. También son importantes, de lo contrario nada funciona, la acción no sube.Como resultado, tenemos un archivo que, en principio, puede escribirse manualmente. Pero a nadie le gusta escribir manualmente. { "hints": [], "groups": [ { "sourceType": "com.ironbank.RavenProperties", "name": "", "type": "com.ironbankRavenProperties" } ], "properties": [ { "sourceType": "com.ironbank.RavenProperties", "name": ".", "type": "java.util.List<java.lang.String>" } ] }
Dirección del cuervo
Hicimos la primera parte de la tarea: obtuvimos algunas propiedades. Pero nadie está relacionado con estas propiedades todavía. Ahora deben establecerse como condición para crear nuestro oyente. @Configuration @EnableConfigurationProperties(RavenProperties.class) public class IronConfiguration { @Bean @ConditionalOnProduction @ConditionalOnProperty(".") public RavenListener ravenListener() { return new RavenListener(); } }
Agregamos una condición más: un cuervo debe crearse solo con la condición de que alguien le diga a dónde volar.Ahora escribiremos a dónde volar en application.yml. spring: application.name: money-raven jpa.hibernate.ddl-auto: validate ironbank: ---: - : -: , : true
Queda por prescribir en la lógica que vuela a donde le dijeron.Para hacer esto, podemos generar un constructor. El nuevo Spring tiene una inyección de constructor, esta es la forma recomendada. A Eugene le gusta @Autowired
hacer que todo aparezca en la aplicación a través de la reflexión. Me encanta seguir las convenciones que ofrece 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); }); } }
Pero no es gratis. Por un lado, obtienes un comportamiento verificable, por otro lado, obtienes algunas hemorroides. @Configuration @EnableConfigurationProperties(RavenProperties.class) public class IronConfiguration { @Bean @ConditionalOnProduction @ConditionalOnProperty(".") public RavenListener ravenListener(RavenProperties r) { return new RavenListener(r); } }
No hay ninguno @Aurowired
, con Spring 4.3 no puedes instalarlo. Si hay un solo constructor, lo es @Aurowired
. En este caso, se usa una anotación @RequiredArgsConstructor
, que genera un solo constructor. Esto es equivalente a este comportamiento: 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 aconseja escribir de esta manera o usar Lombok. Jurgen Holler, que ha estado escribiendo el 80% del código de Spring desde 2002, le recomienda que lo configure @Aurowired
para que sea visible (de lo contrario, la mayoría de las personas miran y no ven ninguna inyección). 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); }); } }
¿Cómo pagamos por este enfoque? Tuvimos que agregar RavenProperties
a la configuración de Java. Y si lo puse @Aurowired
sobre el campo, no habría que cambiar nada.Entonces, los cuervos son enviados. Completamos la tarea, lo que permitió a los usuarios de nuestro iniciador tener cumplidos en sus configuraciones, mientras que obtuvimos un contenedor que se enciende y apaga dependiendo de estas configuraciones.Ley del hierro 1.4. Cuervo personalizado
Sucede que necesita personalizar el comportamiento del iniciador. Por ejemplo, tenemos nuestro propio cuervo negro. Y necesitamos uno blanco que fume, y queremos enviarlo para que la gente vea humo en el horizonte.Pasemos de la alegoría a la vida real. El primer plato me trajo un montón de beans de infraestructura, y eso es genial. Pero no me gusta cómo están configurados. Ingresé a las propiedades de la aplicación y cambié algo allí, y ahora me gusta todo. Pero hay situaciones en las que la configuración es tan complicada que es más fácil registrar la fuente de datos usted mismo que tratar de averiguar las propiedades de la aplicación. Es decir, queremos registrar la fuente de datos nosotros mismos en el contenedor recibido del iniciador. ¿Qué pasará entonces?Yo mismo registré algo y el motor de arranque me trajo mi fuente de datos. ¿Tengo dos ahora? ¿O uno aplastará a uno (y cuál?)Queremos mostrarle otra condición que permita que el iniciador traiga algún tipo de contenedor solo si la persona que usa el iniciador no tiene dicho contenedor. Al final resultó que, esto es completamente no trivial.Hay una gran cantidad de condiciones que ya se nos han hecho: @ConditionalOnBean @ConditionalOnClass @ConditionalOnCloudPlatform @ConditionalOnExpression @ConditionalOnJava @ConditionalOnJndi @ConditionalOnMissingBean @ConditionalOnMissingClass @ConditionalOnNotWebApplication @ConditionalOnProperty @ConditionalOnResource @ConditionalOnSingleCandidate @ConditionalOnWebApplication ...
En principio, @ConditionalOnMissingBean
también lo hay, así que solo use el ya hecho. Vayamos a la configuración, donde indicamos que debe crearse solo si nadie ha creado ese contenedor antes. @Configuration @EnableConfigurationProperties(RavenProperties.class) public class IronConfiguration { @Bean @ConditionalOnProduction @ConditionalOnProperty(".") @ConditionalOnMissingBean</b> public RavenListener ravenListener(RavenProperties r) { return new RavenListener(r); } }
Si abre la mayoría de los iniciadores, verá que cada bin, cada configuración se cuelga con un conjunto de anotaciones. Solo estamos tratando de hacer un análogo.Al intentar lanzar el cuervo no fue, pero apareció el Evento, que escribimos en nuestro nuevo oyente - MyRavenListener
.
Hay dos puntos importantes aquí.El primer punto es que nos enganchamos de nuestro oyente existente y no escribimos ningún oyente allí: @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); }); } }
Segundo: lo hicimos con la ayuda del componente. Si lo hicimos en una configuración de Java, es decir registraría la misma clase que un bean de configuración, nada funcionaría para nosotros.Si limpio extends
y hago algún tipo de escucha de aplicaciones, @ConditionalOnMissingBean
no funcionará. Pero desde También se llama a la clase, cuando intentamos crearla, podemos escribir ravenListener
, tal como lo hicimos en nuestra configuración. Arriba, nos centramos en el hecho de que el nombre del bean en la configuración de Java será por el nombre del método. Y en este caso, creamos un bin llamado ravenListener
.¿Por qué necesitas saber todo esto? Con Spring Boot, todo suele ser excelente, pero solo al principio. Cuando el proyecto avanza, aparece un iniciador, el segundo, el tercero. Empiezas a escribir algunas cosas con tus manos, porque incluso el mejor principiante no te dará lo que necesitas. Y comienzan los conflictos bin. Por lo tanto, es bueno si tiene al menos una idea general de cómo asegurarse de que no se cree un bean y cómo registrar el bean en su hogar para que el iniciador no genere conflictos (o si tiene dos iniciadores que traen uno y el mismo el mismo contenedor para que no entren en conflicto entre ellos). Para resolver el conflicto, estoy escribiendo mi bean, que se asegurará de que no se cree ni el primero ni el segundo.Además, un conflicto de frijoles es una buena situación porque lo ves. Si especificamos los mismos nombres de bin, no tendremos un conflicto. Un bean simplemente sobrescribirá a otro. Y comprenderán durante mucho tiempo dónde está lo que estaba allí. Por ejemplo, si hacemos algún tipo de fuente de datos @Bean
, sobrescribirá la fuente de datos existente @Bean
.Por cierto, si el motor de arranque lleva lo que no necesita, simplemente haga un contenedor con la misma identificación y listo. Es cierto, si el iniciador en alguna versión cambia el nombre del método, entonces eso es todo, su bin nuevamente será dos.ConditionalOnPuzzler
Tenemos @ConditionalOnClass
, @ConditionalOnMissingBean
puede haber clases de escritura. Como ejemplo, considere la configuración de ejecución.Hay jabón, hay una soga, nos colgamos en la horca. Hay una silla y una corriente: es lógico poner a una persona en una silla. Hay una guillotina y un buen humor, eso significa que necesitas cortar cabezas. @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 (" "); } }
¿Cómo vamos a ejecutar?Pregunta: ¿Cómo pueden @ConditionalOnMissingClass
funcionar las anotaciones de tipo en general ?Supongamos que tengo un método que creará una horca. Pero solo se debe crear un contenedor de horca si hay jabón y cuerda. Pero no hay jabón. ¿Cómo puedo entender que no hay jabón o solo cuerda? ¿Qué sucede si intento leer anotaciones de un método, y estas anotaciones se refieren a clases que no lo son? ¿Puedo tomar esas anotaciones?Opciones de respuesta:- ClassDefNotFound? , . - , ClassDefNotFound , reflection- , conditional as long as;
- , . reflection . , .
- ;
- .
: , . reflection . exception, , — . reflection? , , , , — ClassDefNotFound
.
Esto funcionará usando ASM. Al ver eso a través de la reflexión, nada en absoluto, Spring analizará el código de bytes, condicionalmente, manualmente. Él lee el archivo para no descargarlo prematuramente, y entiende lo que hay allí @Conditional
con jabón, cuerda. Ya puede verificar la presencia de estas clases en contexto por separado. Pero ASM, como dicen, no se trata de velocidad. Esta es una oportunidad para leer una clase sin cargarla y comprender la información del método.Pero Juergen Hoeller recomienda no estar vinculado a los nombres de clase, prescribiendo condiciones, a pesar del hecho de que hay una anotación OnMissingClass
que puede tomar el nombre de clase (String) como parámetro. Si sigue esta recomendación, todo funciona más rápido y no se necesita ASM. Pero a juzgar por la fuente, nadie hace eso.Ley del Hierro 1.5. Enciende y apaga el cuervo
Necesitábamos otra propiedad: la capacidad de habilitar o deshabilitar el cuervo manualmente. Para no enviar a nadie garantizado. Esta es la última condición que le mostramos.Nuestro entrante no da nada excepto un cuervo. Por lo tanto, puede preguntar, ¿por qué ser capaz de encenderlo / apagarlo, simplemente no puede tomarlo? Pero en la segunda parte, se incluirán cosas útiles adicionales en este iniciador. Específicamente, un cuervo puede no ser necesario: es costoso, se puede apagar. Al mismo tiempo, no es muy bueno quitar el punto final donde enviarlo, parece una muleta.Por lo tanto, haremos todo a través @ConditionalOnProperty(".")
. @Configuration @EnableConfigurationProperties(RavenProperties.class) public class IronConfiguration { @Bean @ConditionalOnProduction @ConditionalOnProperty(".") @ConditionalOnProperty(".") public RavenListener ravenListener(RavenProperties r) { return new RavenListener(r); } }
Y nos jura que esto no se puede hacer: anotación duplicada. El problema es que si tenemos una anotación con algunos parámetros, no es repetible. No podemos hacer esto en dos propiedades.Tenemos métodos para esta anotación, hay String y esta es una matriz; puede especificar varias propiedades allí. @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; }
Y todo está bien, hasta que intente personalizar el valor de cada elemento de esta matriz por separado. Tenemos una propiedad
, que debería ser false
y es otra propiedad, que debería tener string
cierto valor. Pero puede especificar solo un valor en todas las propiedades.Es decir, no puedes hacer esto: @ConditionalOnProduction @ConditionalOnProperty(name = ".", havingValue="true") @ConditionalOnProperty(name = ".", havingValue="true") @ConditionalOnProperty(name = ".",havingValue="false") public IronBankApplicationListener applicationListener() { ... }
@ConditionalOnProduction @ConditionalOnProperty( name = { ".", ".", "." }, havingValue = "true" ) public IronBankApplicationListener applicationListener() { ... }
Lo resaltado aquí no es una matriz.Sin embargo, hay una perversión que le permite trabajar con varias propiedades, con un solo valor para ellas: AllNestedConditions
y AnyNestedCondition
.
Se ve, francamente, extraño. Pero funciona. Vamos a tratar de hacer que la configuración - el nuevo kondishn, que será tomado en cuenta .
y .
. @Configuration @EnableConfigurationProperties(RavenProperties.class) public class IronConfiguration { @Bean @ConditionalOnProduction @ConditionalOnProperty(".") @ConditionalOnRaven public RavenListener ravenListener(RavenProperties r) { return new RavenListener(r); } }
Ponemos nuestra anotación @Conditional()
y aquí tenemos que registrar alguna clase. @Retention(RUNTIME) @Conditional({OnRavenCondional.class}) public @interface CondionalOnRaven { }
Nosotros lo creamos. public class OnRavenCondional implements Condition { }
Además, tuvimos que implementar algún tipo de condicionamiento, pero es posible que no lo hagamos porque tenemos la siguiente anotación: public class CompositeCondition extends AllNestedConditions { @ConditionalOnProperty( name = ".", havingValue = "false") public static class OnRavenProperty { } @ConditionalOnProperty( name = ".enabled", havingValue = "true", matchIfMissing = true) public static class OnRavenEnabled { } ... }
Tenemos uno compuesto Conditional
, que también se heredará de otra clase, ya sea AllNestedConditions
o AnyNestedCondition
, y contendrá otras clases que contienen las anotaciones habituales con condimentos. Es decir
en su lugar @Condition
debemos especificar: public class OnRavenCondional extends AllNestedConditions { public OnRavenCondional() { super(ConfigurationPhase.REGISTER_BEAN); } }
En este caso, debe crear un constructor dentro.Ahora tenemos que crear clases estáticas aquí. Hacemos algún tipo de clase (llamémosla R). public class OnRavenCondional extends AllNestedConditions { public OnRavenCondional() { super(ConfigurationPhase.REGISTER_BEAN); } public static class R {} }
Hacemos nuestro valor
(debe ser exactamente 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 {} }
Para repetir esto, solo recuerda el nombre de la clase. Spring tiene buenos muelles Java. Puede abandonar IDEA, leer el dock de Java y comprender lo que debe hacerse.Nosotros ponemos el nuestro @ConditionalOnRaven
. En principio, puede ajustar ambos @ConditionalOnProduction
, y , y @ConditionalOnMissingBean
, pero ahora no haremos esto. Solo mira lo que pasó. @Configuration @EnableConfigurationProperties(RavenProperties.class) public class IronConfiguration { @Bean @ConditionalOnProduction @ConditionalOnRaven @ConditionalOnMissingBean public RavenListener ravenListener(RavenProperties r) { return new RavenListener(r); } }
En ausencia de un
cuervo no debe volar. El no voló.No quiero apostar
, porque primero debemos hacer un autocompletado; este es uno de nuestros requisitos. @Data @ConfigurationalProperties("") public class RavenProperties { List<String> ; boolean ; }
Eso es todo.
false
,
true
application.yml:
jpa.hibernate.ddl-auto: validate ironbank: ---: - : : , : true
, .
, repeatable. Java. .
, , .
Minuto de publicidad. 19-20 Joker 2018,
« [Joker Edition]» ,
«Micronaut vs Spring Boot, ?» . En general, habrá muchos más informes interesantes y notables en Joker. Las entradas se pueden comprar en el
sitio web oficial de la conferencia.
¡Y también tenemos una pequeña encuesta para ti!