Share
Embed
Spring boot/cloud workshop at jpoint 2018
Part 1: Spring Boot
* How to cook microservice
* Theory
* Modeling with Lombok
* Jackson configuration
* RestTemplate and advanced configuration
Part 2: Spring Boot
* How to cook microserviceS
* Serivce Discovery (Eureka)
* Config Server (+Spring Cloud Bus)
* Configuration live update
* Gateway (Zuul)
* Monitoring (Hystrix+Zipkin)
Kirill Tolkachev
April 08, 2018
Transcript
320 slides
-
web.xml <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>mvc-dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>mvc-dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> -
… <context-param> <param-name>contextClass</ param-name> <param-value>…AnnotationConfigWebApplicationContext</ param-value> </context-param> <context-param> <param-name>contextConfigLocation</ param-name>
<param-value>com.inwhite.conf.AppConfig</ param-value> </context-param> … -
Deployment →Use maven/gradle to build a war from that project
→Copy it to webapps directory of tomcat -
create Spring ContextLoaderListener start applicationContext Create filter mapping Create security
filter login Create spring dispatcher servlet Delegate to Controller -
§ 8.2.4 Shared libraries / runtimes pluggability The ServletContainerInitializer class
is looked up via the jar services API. For each application, an instance of the ServletContainerInitializer is created by the container at application startup time. The framework providing an implementation of the ServletContainerInitializer MUST bundle in the META-INF/services directory of the jar file a file called javax.servlet.ServletContainerInitializer, as per the jar services API, that points to the implementation class of the ServletContainerInitializer. 3.+ -
§ 8.2.4 Shared libraries / runtimes pluggability The ServletContainerInitializer’s onStartup
method get's a Set of Classes that either extend / implement the classes that the initializer expressed interest in or if it is annotated with any of the classes specified via the @HandlesTypes annotation. 3.+ -
Как? SPI tomcat |→ get javax.servlet.ServletContainerInitializer impl via SPI |→
get all MyInitializer classes (from @HandlesTypes) -
Как? SPI tomcat |→ get javax.servlet.ServletContainerInitializer impl via SPI |→
get all WebApplicationInitializer classes (from @HandlesTypes) org.springframework.web.WebApplicationInitializer -
Как? SPI tomcat |→ get javax.servlet.ServletContainerInitializer impl via SPI |→
get all WebApplicationInitializer classes (from @HandlesTypes) |→ call ServletContainerInitializer.onStartup(classes,servletCtx) according to @Order order -
@HandlesTypes(WebApplicationInitializer.class) public class SpringServletContainerInitializer implements ServletContainerInitializer { @Override public void
onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) -
@HandlesTypes(WebApplicationInitializer.class) public class SpringServletContainerInitializer implements ServletContainerInitializer { @Override public void
onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) ... AnnotationAwareOrderComparator.sort(initializers); ... -
@HandlesTypes(WebApplicationInitializer.class) public class SpringServletContainerInitializer implements ServletContainerInitializer { @Override public void
onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) ... AnnotationAwareOrderComparator.sort(initializers); ... initializers.forEach(initializer -> initializer.onStartup(ctx)); } -
public class WebStarter implements WebApplicationInitializer { @Override public void onStartup(ServletContext
servletContext) throws ServletException { AnnotationConfigWebApplicationContext webContext = new AnnotationConfigWebApplicationContext(); webContext.register(WebConfig.class); ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcher", new DispatcherServlet(webContext)); servlet.setLoadOnStartup(1); AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext(); appContext.register(AppConfig.class); servletContext.addListener(new ContextLoaderListener(appContext)); servlet.addMapping("/*"); } -
Как выглядит наш build.gradle plugins { id "org.springframework.boot" version "1.5.3.RELEASE"
} dependencies { compile 'org.springframework.boot:spring-boot-starter-web' } Добавляем зависимости -
Как выглядит наш build.gradle plugins { id "org.springframework.boot" version "1.5.3.RELEASE"
} dependencies { compile 'org.springframework.boot:spring-boot-starter-web' } Automatic Dependency Management -
Как выглядит наш build.gradle plugins { id "org.springframework.boot" version "1.5.3.RELEASE"
} dependencies { compile 'org.springframework.boot:spring-boot-starter-web' } dependencyManagement { imports { mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Dalston.RELEASE' } } -
Анатомия SpringBoot Jar jar |- META-INF |- BOOT-INF |- libs
|- classes |- org |- springframework |- boot -
java -jar myapp.jar myapp.jar |- META-INF |- BOOT-INF |- libs
|- classes |- org |- springframework |- boot MANIFEST.MF ... Main-Class: ??? ... -
Анатомия SpringBoot Jar myapp.jar |- META-INF |- BOOT-INF |- libs
|- classes |- org |- springframework |- boot MANIFEST.MF ... Spring-Boot-Version: 1.5.3.RELEASE Implementation-Vendor: Pivotal Software, Inc. Main-Class: org.springframework.boot.loader.JarLauncher Start-Class:ru….ripper.OurMainClass Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ ... -
myapp.jar |- META-INF |- BOOT-INF |- libs |- classes |-
org |- springframework |- boot Анатомия SpringBoot Jar MANIFEST.MF ... Spring-Boot-Version: 1.5.3.RELEASE Implementation-Vendor: Pivotal Software, Inc. Main-Class: org.springframework.boot.loader.JarLauncher Start-Class:ru….ripper.OurMainClass Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ ... -
MANIFEST.MF ... Spring-Boot-Version: 1.5.3.RELEASE Implementation-Vendor: Pivotal Software, Inc. Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class:ru….ripper.OurMainClass Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ ... myapp.jar |- META-INF |- BOOT-INF |- libs |- classes |- org |- springframework |- boot Анатомия SpringBoot Jar JarLauncher -
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <mainClass>conference...OurMainClass</mainClass> </plugin> </plugins> </build> MANIFEST.MF
... Start-Class: conference.spring.boot.ripper.OurMainClass ... -
Как выбирается mainClass если его не указать Spring boot plugin
сканирует проект – ищет мейны • если есть только один – то это он :) • если больше одного – смотрит где стоит @SpringBootApplication и выбирает его • если @SpringBootApplication нет или >1 – ошибка о множественных main при сборке -
Хочу executable jar: Maven <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration>
<executable>true</executable> </configuration> </plugin> </plugins> </build> springBoot { executable = true } Gradle -
Внутренний мир executable Jar • Бабушка хочет чтобы жмя и
работало • windows “click click” • $ ./app.jar • ... -
Как выглядит Jar? Script 0xf4ra -> а это ZIP! jar
archive Начало файла -
Как выглядит Jar? Script 0xf4ra -> а это ZIP! jar
archive Начало файла Начало zip архива -
Где контекс? @SpringBootApplilcation class App { public static void main(String[]
args) { SpringApplication.run(App.class,args); } } -
Вот контекст! @SpringBootApplilcation class App { public static void main(String[]
args) { ApplicationContext context = SpringApplication.run(App.class,args); } } -
Давайте поговорим о контексте Малыш знает про ClassPathXmlApplicationContext А какие
контексты знаешь ТЫ? -
Это я решаю какой контекст создать Я до создания контекста
ещё много всего делаю, но об этом потом. SpringApplication -
Что за spring.factories ? § 44.1 Understanding auto-configured beans Under
the hood, auto-configuration is implemented with standard @Configuration classes. Additional @Conditional annotations are used to constrain when the auto-configuration should apply. Usually auto-configuration classes use @ConditionalOnClass and @ConditionalOnMissingBean annotations. This ensures that auto-configuration only applies when relevant classes are found and when you have not declared your own @Configuration. You can browse the source code of spring-boot-autoconfigure to see the @Configuration classes that we provide (see theMETA-INF/spring.factories file). -
@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 { … -
@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 { … -
@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 @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 @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 {}; } ImportSelector -
SpringFactoriesLoader static <T> List<T> loadFactories( Class<T> factoryClass, ClassLoader cl )
static List<String> loadFactoryNames( Class<?> factoryClass, ClassLoader cl ) -
CacheConfigurations 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); } -
Где то внутри CacheAutoConfiguration for (int i = 0; i
< types.length; i++) { Imports[i] = CacheConfigurations.getConfigurationClass(types[i]); } return imports; -
Conditional и друзья @ConditionalOnBean @ConditionalOnClass @ConditionalOnCloudPlatform @ConditionalOnExpression @ConditionalOnJava @ConditionalOnJndi @ConditionalOnMissingBean
@ConditionalOnMissingClass @ConditionalOnNotWebApplication @ConditionalOnProperty @ConditionalOnResource @ConditionalOnSingleCandidate @ConditionalOnWebApplication ... -
Паззлер @Configuration @ConditionalOnWinterIsHere public class UndeadArmyConfiguration { @Bean public WinterUndeadArmy
cursedArmy() { return new WinterUndeadArmy(); } @Bean @ConditionalOnWinterIsHere public DragonGlassFactory dragonGlassFactory() { return new DragonGlassFactory(); } -
ConditionalOnPuzzler @Configuration public class КонфигурацияКазни { @Bean @ConditionalOnClass ({Мыло.class, Веревка.class})
@ConditionalOnMissingBean ({ФабрикаКазни. class}) public ФабрикаКазни виселицы() { return new ФабрикаВиселиц( "..."); } } -
ConditionalOnPuzzler @Configuration public class КонфигурацияКазни { @Bean @ConditionalOnClass ({Мыло.class, Веревка.class})
@ConditionalOnMissingBean ({ФабрикаКазни. class}) public ФабрикаКазни виселицы() { return new ФабрикаВиселиц( "..."); } @Bean @ConditionalOnClass ({Стул.class, Ток.class}) @ConditionalOnMissingBean ({ФабрикаКазни. class}) public ФабрикаКазни cтулья() { return new ФабрикаЭлектрическихСтульев( "вж вж"); } } -
ConditionalOnPuzzler @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 ФабрикаГильотин( "хрусть хрусть"); } } -
ConditionalOnPuzzler @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 ФабрикаГильотин( "хрусть хрусть"); } } 1. ClassDefNotFound ? 2. Так вообще нельзя, не компилируется 3. Будет работать 4. Будет отлично работать -
ConditionalOnPuzzler @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 ФабрикаГильотин( "хрусть хрусть"); } } 1. ClassDefNotFound ? 2. Так вообще нельзя, не компилируется 3. Будет работать 4. Будет отлично работать -
OnPropertyCondition @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; } Что если нужно изменить? -
public class WebStarter implements WebApplicationInitializer { @Override public void onStartup(ServletContext
servletContext) throws ServletException { AnnotationConfigWebApplicationContext webContext = new AnnotationConfigWebApplicationContext(); webContext.register(WebConfig.class); ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcher", new DispatcherServlet(webContext)); servlet.setLoadOnStartup(1); AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext(); appContext.register(AppConfig.class); servletContext.addListener(new ContextLoaderListener(appContext)); servlet.addMapping("/*"); } -
public class WebStarter implements WebApplicationInitializer { @Override public void onStartup(ServletContext
servletContext) throws ServletException { AnnotationConfigWebApplicationContext webContext = new AnnotationConfigWebApplicationContext(); webContext.register(WebConfig.class); ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcher", new DispatcherServlet(webContext)); servlet.setLoadOnStartup(1); AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext(); appContext.register(AppConfig.class); servletContext.addListener(new ContextLoaderListener(appContext)); servlet.addMapping("/*"); } -
public class WebStarter implements WebApplicationInitializer { @Override public void onStartup(ServletContext
servletContext){ SpringApplication.run(RipperApplication.class); } } -
public class WebStarter implements WebApplicationInitializer { @Override public void onStartup(ServletContext
servletContext) { SpringApplication.run(RipperApplication.class); } } @SpringBootApplication public class RipperApplication { public static void main(String[] args) { SpringApplication.run(RipperApplication.class, args); } } -
public class WebStarter implements WebApplicationInitializer{ @Override public void onStartup(ServletContext servletContext)
throws ServletException { SpringApplication.run(RipperApplication.class); } } Tomcat в Tomcat`е -
§ 85.1 Create a deployable war file @SpringBootApplication public class
Application extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder app) { return app.sources(Application.class); } public static void main(String[] args) throws Exception { SpringApplication.run(Application.class, args); } } -
§ 85.1 Create a deployable war file @SpringBootApplication public class
Application extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder app) { return app.sources(Application.class); } public static void main(String[] args) throws Exception { SpringApplication.run(Application.class, args); } } -
§ 85.1 Create a deployable war file @SpringBootApplication public class
Application extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder app) { return app.sources(Application.class); } public static void main(String[] args) throws Exception { SpringApplication.run(Application.class, args); } } Работает по разному -
§ 85.1 Create a deployable war file @SpringBootApplication public class
Application extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder app) { return app.sources(Application.class); } public static void main(String[] args) throws Exception { SpringApplication.run(Application.class, args); } } -
§ 85.1 Create a deployable war file @SpringBootApplication public class
Application extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder a) { return application.sources(Application.class); } public static void main(String[] args) throws Exception { SpringApplication.run(Application.class, args); } } Никогда не запустится в tomcat Только java -jar -
§ 85.1 Create a deployable war file @SpringBootApplication public class
Application extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder a) { return application.sources(Application.class); } public static void main(String[] args) throws Exception { SpringApplication.run(Application.class, args); } } Без main не соберется см. maven/gradle плагины Обязательно -
§ 85.1 Create a deployable war file @SpringBootApplication public class
Application extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder a) { return application.sources(Application.class); } public static void main(String[] args) throws Exception { SpringApplication.run(Application.class, args); } } Опционально -
§ 85.1 Create a deployable war file @SpringBootApplication public class
Application extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder a) { return application.sources(Application.class); } public static void main(String[] args) throws Exception { SpringApplication.run(Application.class, args); } } Никогда не запустится в embedded режиме Только в Tomcat контейнере Опционально -
Анатомия War .war |- META-INF |- WEB-INF |- lib |-
classes |- org/springframework/boot/loader |- WarLauncher and friends -
А как убрать tomcat из tomcat? If you are using
a version of Gradle that supports compile only dependencies (2.12 or later), you should continue to use providedRuntime. Among other limitations, compileOnly dependencies are not on the test classpath so any web-based integration tests will fail. -
Анатомия War .war |- META-INF |- WEB-INF |- lib |-
lib-provided |- classes |- org/springframework/boot/loader |- WarLauncher and friends -
Анатомия SpringBoot Jar/War jar |- META-INF |- BOOT-INF |- libs
|- classes |- org/springframework/boot/loader |- JarLauncher and friends war |- META-INF |- WEB-INF |- lib |- lib-provided |- classes |- org/springframework/boot/loader |- WarLauncher and friends -
{ "География":5, "Физика" :3 } Физика Математика Теология Не грешно
ли лезть в атом? Теорема Пифагора Неравенство Коши ваы Количество вопросов Количество вопросов Экзаминатор -
С чего начинают строить микросервис →Контроллеры →Сервисы →Dao →Model –
поскольку модель используется на всех слоях, пожалуй лучше начинать с неё -
in: { "student": "Борисов Евгений", "title": "Экзамен на JokerConf", "sections":
[{ "title": "Java", "description": "простые вопросы по java", "exercises": [ { "question": "разница между spring string и swing", "answer": "без spring`а не обойтись" }, { "question": "разница между final finally finalize", "answer": "за finalize отрывают руки" } ]}, { "title": "философия", "description": "сложные вопросы по философии", "exercises": [{ "question": "В чем смысл жизни", "answer": "42" }, { "question": "Что раньше, яйцо или курица", "answer": "петух" }] }] } out: { "mark": 99, "title": "Экзамен на JokerConf", "sections": [{ "title": "Java", "description": "простые вопросы по java", "exercises": [ { "question": "разница между spring string и swing", "answer": "без spring`а не обойтись" }, { "question": "разница между final finally finalize", "answer": "за finalize отрывают руки" } ]}, { "title": "философия", "description": "сложные вопросы по философии", "exercises": [{ "question": "В чем смысл жизни", "answer": "42" }, { "question": "Что раньше, яйцо или курица", "answer": "петух" }] }] } -
Data model - задание Придумайте архитектуру модели, которая позволит написать
Сервис: - принимающий на вход объект SolvedExam - отдающий CheckedExam с оценкой - Должны проходить тесты @Test public void checkExamineContract() throws Exception { ... mockMvc.perform(post("/examine")) ... } Use @RestController and @RequestMapping > @*Mapping Luke -
@RestController("/examine") Это имя spring bean, не http path И это
даже может работать: • Если нет ни одного контроллера с путём (всегда будет вызываться) • Если потом указали полный путь в @RequestMapping -
@RequestMapping и его братья Управляет маршрутизацией запросов в метод контроллера.
Маршрутизирует по: • name/value – сам http path • method – http method • params • headers -
@RequestMapping и его братья @PostMapping("/examine") public CheckedExam ex`( @RequestBody SolvedExam
title) { } @RequestMapping (path = "/examine", method = POST) public CheckedExam ex2( @RequestBody SolvedExam title) { } @RequestMapping (path = "/examine", method = POST, headers = {"my-app=appid"}) public CheckedExam ex3( @RequestBody SolvedExam title) { } @GetMapping(path = "/examine", headers = {"content-type=application/json"}) public CheckedExam ex3() { } @GetMapping(path = "/examine", produces = MimeTypeUtils. APPLICATION_JSON_VALUE) public CheckedExam ex3() { } -
@RequestMapping и его братья @PostMapping("/examine") public CheckedExam ex`( @RequestBody SolvedExam
title) { } @RequestMapping (path = "/examine", method = POST) public CheckedExam ex2( @RequestBody SolvedExam title) { } @GetMapping(path = "/examine", headers = {"content-type=application/json"}) public CheckedExam ex3() { } @GetMapping(path = "/examine", produces = APPLICATION_JSON_VALUE) public CheckedExam ex3() { } @GetMapping(path = "/examine", produces = APPLICATION_JSON_VALUE) public CheckedExam ex4() { } -
@RequestMapping и его братья @PostMapping("/examine") public CheckedExam ex`( @RequestBody SolvedExam
title) { } @RequestMapping (path = "/examine", method = POST) public CheckedExam ex2( @RequestBody SolvedExam title) { } @GetMapping(path = "/examine", headers = {"content-type=application/json"}) public CheckedExam ex3() { } @GetMapping(path = "/examine", produces = APPLICATION_JSON_VALUE) public CheckedExam ex3() { } @GetMapping(path = "/examine", produces = APPLICATION_JSON_VALUE) public CheckedExam ex4() { } -
@RequestMapping и его братья //curl -XPOST /examine -d '{ "title":
{..} }' @PostMapping("/examine") public CheckedExam ex`( @RequestBody SolvedExam exam) { } //curl -XGET /examine/ mytitle @GetMapping("/examine/{title}") public CheckedExam ex3( @PathVariable String title) { } //curl -XGET /examine/mytitle ?debug=true @GetMapping("/examine/{title}") public CheckedExam ex3( @PathVariable String title, @RequestParam Boolean debug) { } -
Data model - задание Придумайте архитектуру модели, которая позволит написать
Сервис: - принимающий на вход объект SolvedExam - отдающий CheckedExam с оценкой - Смотрите в src/test/resources/*.json - Должны проходить тесты @Test public void checkExamineContract() throws Exception { ... mockMvc.perform(post("/examine")) ... } Use @RestController and @RequestMapping > @*Mapping Luke -
Lombok – composition / delegate and friends Annotation Processor Работает
на этапе компиляции Генерит код, чтобы мы не писали Делает джаву более похожим на нормальный язык -
Про lombok, модели и не только @Data – POJO (@Getter,
@Setter, @ToString, @EqualsAndHashcode) @Value – immutable POJO @AllArgumentConstruct(onConstructor = @_(@Autowired)) -
Про lombok, модели и не только @Data – POJO (@Getter,
@Setter, @ToString, @EqualsAndHashcode) @Value – immutable POJO @AllArgumentConstruct(onConstructor = @_(@Autowired)) @RequireArgumentConstructor /@NoArgumentConstructor @Builder / @Singular @Delegate – примерно как в груви @SneakyThrows @Slf4j / @Log4j / … -
Как Джексон пишет в джейсон 1. Использует Java Getters a.
Методы начинающиеся с Get 2. @JsonIgnore – игнорируем Getter 3. Нет ни одного Getter – падает -
Как Джексон пишет в объект 1. Нет конструктора с @ConstructProperies
→ выставляет через Setters → если нет Setter и есть Getter – выставляет напрямую в филды → это все только при наличии пустого конструктора 2. Есть конструктор с @ConstructProperies → выставляет значение через него 3. После зачем то вызовет все геттеры * Но если есть то @JsonIgnore всё по другому * Это поведение по умолчанию -
→ Если вы настраиваете руками: ObjectMapper mapper = new ObjectMapper()
.registerModule( new ParameterNamesModule()) .registerModule( new Jdk8Module()) .registerModule( new JavaTimeModule()); → Или так: mapper.findAndRegisterModules(); Не забыть зависимости: compile 'com.fasterxml.jackson.module:jackson-module-parameter-names' compile 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8' compile 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' C последних версий есть поддержка Java8 -
Как Spring MVC работает с Джексоном Напишите метод который принимает
json и класс, а возвращает объект данного класса Пробуем использовать objectMapper Jackson2 -
Задание 2 Микросервис – Теологии 1. Добавлять вопросы по теологии
… (CRUD) 2. Получать нужное количество рандомальных вопросов 3. Тест должен проходить @Test public void should_return_random_exercise() throws Exception { mvc.perform(get("/exercise/random?count=3") .contentType( APPLICATION_JSON) ) … Use JpaRepository and @RequestMapping Luke -
{ "География":5, "Физика" :3 } Физика Математика Теология Не грешно
ли лезть в атом? Теорема Пифагора Неравенство Коши ваы Количество вопросов Количество вопросов Экзаминатор -
in: { "student": "Борисов Евгений", "title": "Экзамен на JokerConf", "sections":
[{ "title": "Java", "description": "простые вопросы по java", "exercises": [ { "question": "разница между spring string и swing", "answer": "без spring`а не обойтись" }, { "question": "разница между final finally finalize", "answer": "за finalize отрывают руки" } ] }, { "title": "философия", "description": "сложные вопросы по философии", "exercises": [{ "question": "В чем смысл жизни", "answer": "42" }, { "question": "Что раньше, яйцо или курица", "answer": "петух" }] }] } -
Дальше пишем Entity + Repository Which repositories get exposed by
defaults? Name Description DEFAULT Exposes all public repository interfaces but considers @Repository/@RestResource’s `exported flag ALL Exposes all repositories independently of type visibility and annotations ANNOTATION Only repositories annotated with @Repository/@RestResource are exposed, unless their exported flag is set to false VISIBILITY Only public repositories annotated are exposed -
Как поменять стратегию @Component public class MyWebConfiguration extends RepositoryRestConfigurerAdapter {
@Override public void configureRepositoryRestConfiguration( RepositoryRestConfiguration config) { config.setRepositoryDetectionStrategy( ALL); } } -
Как задать URL @Configuration class CustomRestMvcConfiguration { @Bean public RepositoryRestConfigurer
repositoryRestConfigurer() { return new RepositoryRestConfigurerAdapter() { @Override public void configureRepositoryRestConfiguration( RepositoryRestConfiguration config) { configuration.setBasePath( "/api") } }; } } -
Как задать URL @Configuration class CustomRestMvcConfiguration { @Bean public RepositoryRestConfigurer
repositoryRestConfigurer() { return new RepositoryRestConfigurerAdapter() { @Override public void configureRepositoryRestConfiguration( RepositoryRestConfiguration config) { configuration.setBasePath( "/api") } }; } } Или в том же RepositoryRestConfigurerAdapter прописать в application.properties - spring.data.rest.base-path=/api -
Spring Data Rest official supports →Spring Data JPA →Spring Data
MongoDB →Spring Data Neo4j →Spring Data GemFire →Spring Data Cassandra -
@RepositoryRestResource @RepositoryRestResource (collectionResourceRel = "people", path = "people") public interface
PersonRepository extends MongoRepository<Person, String> { @RestResource(path = "byname") List<Person> findByLastName(@Param( "name") String name); } -
URL’s и методы localhost:8080/exercise – список всех (GET) localhost:8080/exercise/1 –
дай человека с айдишником 1 (GET) localhost:8080/exercise/1 – стереть с айдишником 1 (DELETE) localhost:8080/exercise/1 – заменить с айдишником 1 (PUT) localhost:8080/exercise/1 – проапдэйтить с айдишником 1 (PATCH) localhost:8080/exercise/search/findByName?name=Lanister -
{ "География":5, "Физика" :3 } Физика Математика Теология Не грешно
ли лезть в атом? Теорема Пифагора Неравенство Коши ваы Количество вопросов Количество вопросов Экзаминатор -
Задание 3 Связать сервисы Экзаменатора и Предметы • Запустить и
проверить самим • Должен отдаваться запрос с ответами другого сервиса { "География":5, "Физика" :3 } -
--server.port Конфликт портов решается с помощью relaxed properties export SERVER_PORT=8081
java -jar --server.port=8081 SPRING_APPLICATION_JSON = '{"server":{"port":8081}}' -
Подход №1 - общий код Зачем разбивать микросервис на модули?
→ Java API для сервисов → фиксация контракта на уровне зависимостей языка → утилитарная функция, избавления от бойлерплейта -
Подход №1 - общий код project |- project-sdk |- project-app
|- project-... -
Подход №1 - общий код project |- project-sdk |- project-app
|- project-api |- project-domain |- project-... Пфф -
Подход №1 - общий код project |- project-sdk |- project-app
|- project-api |- project-domain |- project-... Пфф KISS -
Подход №2 - общий контракт → разделение кода и контракта
→ поставка контракта отдельно -
Обратимся к истории → REST подход ◦ анархия и рекомендации
→ RPC подход ◦ -
Обратимся к истории → REST подход ◦ анархия и рекомендации
→ RPC подход ◦ jax-ws/jax-rpc ◦ corba ◦ json rpc ◦ ◦ -
Обратимся к истории → REST подход ◦ анархия и рекомендации
→ RPC подход ◦ jax-ws/jax-rpc ◦ corba ◦ json rpc ◦ thrift ◦ protobuf/grpc ◦ etc -
Задание 5 • пишем SDK для вызова микросервиса по теологии
• и тесты для него • тестируем контракт • тестируем взаимодействие ◦ WireMock/Spring MockServer ◦ Spring Boot Test ◦ Context Scanning behaviour Тест SDK Тест Контракта Тест Взаимодействия -
{ "География":5, "Физика" :3 } Физика Математика Теология Не грешно
ли лезть в атом? Теорема Пифагора Неравенство Коши Энтропия не? Количество вопросов Количество вопросов Экзаминатор -
{ "География":5, "Физика" :3 } Физика Математика Теология Теорема Пифагора
Неравенство Коши ваы Количество вопросов Количество вопросов Экзаминатор Бог Не грешно ли лезть в атом? -
Матмематика – сервис • Сделать новый модуль • Реализовать метод
"/exercise/random" • Возвращать список автогенерируемых упражнение (как в Теологии) -
Как воткнуть в Examinator math-service? exercises: urls: "theology" : "http://localhost:8080/"
"math" : "http://localhost:8082/" Потому что на 8081 Examinator -
Как воткнуть в Examinator math-service? exercises: urls: "theology" : "http://localhost:8080/"
"math" : "http://localhost:8082/" "examinator": "http://localhost:8081/" "..." : "http://localhost:8083/" Service Registry в application.yml -
{ "География":5, "Физика" :3 } Физика Математика Количество вопросов Количество
вопросов Экзаминатор Экзаминатор Теология Discovery Server -
{ "География":5, "Физика" :3 } Физика Математика Количество вопросов Количество
вопросов Экзаминатор Экзаминатор Теология Discovery Server Eureka Clients Eureka Server -
Your own Discovery Server @EnableEurekaServer @SpringBootApplication public class EurekaServer {
public static void main(String[] args) { SpringApplication.run(EurekaServer.class, args); } } -
Переводим на Eureka • Создаём модуль eureka-server compile 'org.springframework.cloud:spring-cloud-starter-eureka-server' compile
'org.springframework.cloud:spring-cloud-starter-eureka' -
Переводим на Eureka • Создаём модуль eureka-server compile 'org.springframework.cloud:spring-cloud-starter-eureka-server' compile
'org.springframework.cloud:spring-cloud-starter-eureka' • Интегрируем клиента (examinator-service, theology-service, math-service) compile 'org.springframework.cloud:spring-cloud-starter-eureka' -
Переводим на Eureka • Создаём модуль eureka-server compile 'org.springframework.cloud:spring-cloud-starter-eureka-server' compile
'org.springframework.cloud:spring-cloud-starter-eureka' • Интегрируем клиента (examinator-service, theology-service, math-service) compile 'org.springframework.cloud:spring-cloud-starter-eureka' compile 'org.springframework.boot:spring-boot-starter-actuator' @EnableDiscoveryClient vs @EnableEurekaClient eureka.client.service-url.default-zone: http://127.0.0.1:8761/eureka/ -
Переводим на Eureka • Создаём модуль eureka-server compile 'org.springframework.cloud:spring-cloud-starter-eureka-server' compile
'org.springframework.cloud:spring-cloud-starter-eureka' • Интегрируем клиента (examinator-service, theology-service, math-service) compile 'org.springframework.cloud:spring-cloud-starter-eureka' compile 'org.springframework.boot:spring-boot-starter-actuator' @EnableDiscoveryClient vs @EnableEurekaClient eureka.client.service-url.default-zone: http://127.0.0.1:8761/eureka/ -
@EnableDiscoveryClient Будет включен по умолчанию при добавлении стартера c Edgware
spring-cloud release train -
Кто это у нас там спрятался? Смотрим какие сервисы загрузились
$ http :8082/health { "description": "Remote status from Eureka server", "status": "DOWN" } { "description": "Spring Cloud Eureka Discovery Client", "status": "UP" } -
Кто это у нас там спрятался? Выключаем security для управляющий
API management: security: enabled: false Теперь $ http :8082/health -
{ "description": "Spring Cloud Eureka Discovery Client", "discoveryComposite": { "description":
"Spring Cloud Eureka Discovery Client", "discoveryClient": { "description": "Spring Cloud Eureka Discovery Client", "services": [ "examinator-service", "math-service", "theology-service" ], "status": "UP" }, "eureka": { "applications": { "EXAMINATOR-SERVICE": 1, "MATH-SERVICE": 1, "THEOLOGY-SERVICE": 1 }, "description": "Remote status from Eureka server", "status": "UP" }, "status": "UP" }, "diskSpace": { "free": 190254260224, "status": "UP", "threshold": 10485760, "total": 499055067136 }, ... -
True Discovery • Изменить логику резолва сервиса использую имя сервиса
Use @LoadBalanced or LoadBalancerInterceptor Luke -
Discovery и Load Balancing Две стороны одного и того же
Server Side vs Client Side -
238 Service Client Registry-aware HTTP Client Service Registry Service Instance
1 Service Instance N Service Instance ... Load balance request Client side discovery Ribbon + Eureka Discovery client -
239 Service Client Service Registry Service Instance 1 Service Instance
N Service Instance ... Load balance request Router/Proxy Server side discovery -
Принципы SOA 242 1. Standardized service contract 2. Loose coupling
3. Encapsulation 4. Reusability 5. Autonomy 6. Statelessness 7. Discoverability -
Принципы SOA 243 1. Standardized service contract 2. Loose coupling
3. Encapsulation 4. Reusability 5. Autonomy 6. Statelessness 7. Discoverability -
Fluent annotations 244 @Getter // generate getters @Setter // generate
setters @Aspect // we are an aspect @ToString // generate toString() @EnableWs // SOAP is so enterprisy, we definitely need it @Endpoint // Seriously, just read above @EnableWebMvc // we want MVC @EnableCaching // and we want to cache stuff @Configuration // this class can configure itself @RestController // we want some REST @XmlRootElement // this component is marshallable @EnableWebSocket // we want web socket, it's so new-generation @RedisHash("cat") // this class is an entity saved in redis @EnableScheduling // we want scheduled tasks @EnableWebSecurity // and some built-in security @NoArgsConstructor // generate no args constructor @ContextConfiguration // we want context configuration for unit testing @SpringBootApplication // this is a Sprint Boot application @Accessors(chain = true) // getters/setters are chained (ala jQuery) @EnableAspectJAutoProxy // we want AspectJ auto proxy @EnableAutoConfiguration // and auto configuration -
Парадокс централизации Чтобы эффективно разрабатывать распределённые приложения, нам нужны очень
хорошие централизованные библиотеки и инструменты Например: логирование, health-checking, метрики, обработка типовых ошибок, автодокументирование 245 -
Парадокс централизации Чтобы эффективно разрабатывать распределённые приложения, нам нужны очень
хорошие централизованные библиотеки и инструменты Но: не выносите бизнес-логику или доменные объекты! Не размывайте бизнес-контекст вашего API 246 -
Your own Config Server • Создать модуль config-server compile 'org.springframework.cloud:spring-cloud-config-client'
@EnableConfigServer spring: cloud: config: server: git: uri: https://github.com/lavcraft/spring-boot-and-cloud-workshop-joker2017-configs.git -
Проблема курица и яйца Откуда брать координаты eureka? • из
сonfig-server (для всех же одинаковые) • из локальных настроек (ENV/*.yml/etc) Откуда брать координаты config-server • из eureka • из локальных настроек (ENV/*.yml/etc) -
Config Server Discovery Через Service Discovery App Instance #0 App
Instance #N Config Server По прямой ссылке Eureka -
Config Server Discovery Через Service Discovery App Instance #0 App
Instance #N Eureka Config Server register on start get Config Server by name -
Config Server Discovery Через Service Discovery App Instance #0 App
Instance #N Eureka Config Server register on start return config server instances location get Config Server by name -
Config Server Discovery Через Service Discovery App Instance #0 App
Instance #N Eureka Config Server register on start return config server instances location get Config Server by name get configs for App -
Config Server Discovery По прямой ссылке App Instance #0 App
Instance #N Config Server get configs for App Http Load Balancer -
Config Client • интегрируем config-server и *-service через eureka •
включаем/отключаем management.security -
Начнём с примера @RestController @RefreshScope public class TestController { @Value("${foo.bar}")
String test; @GetMapping("/foo") public String getFooBar() { return test; } } -
Начнём с примера @RestController @RefreshScope public class TestController { @Value("${foo.bar}")
String test; @GetMapping("/foo") public String getFooBar() { return test; } } -
Начнём с примера @RestController @RefreshScope public class TestController { @Value("${foo.bar}")
String test; @GetMapping("/foo") public String getFooBar() { return test; } } -
Три простых шага 1. Ставим @RefreshScope над бином который нужно
обновлять 2. Обновляем в config server foo.bar: some val a. Можно не править а дёрнуть /env если есть actuator 3. Дёргаем у приложения /refresh a. Для этого нужен actuator -
Физика Математика Экзаминатор Экзаминатор Теология Теология Config Server ALL APP
Discovery Server ALL APP -
Физика Математика Экзаминатор Экзаминатор Теология Теология Queue Config Server ALL
APP Discovery Server ALL APP ALL APP -
Spring Cloud Bus dependencyManagement { imports { mavenBom 'org.springframework.cloud:spring-cloud-bus:1.3.1.RELEASE' }
} dependencies { compile 'org.springframework.cloud:spring-cloud-starter-bus-amqp' } -
Теперь достаточно /refresh на config-server 1. Все приложения обновят конфигурации
при получении refresh ивента в rabbitmq -
Теперь достаточно /refresh на config-server 1. Все приложения обновят конфигу
при получении refresh ивента в rabbitmq 2. Для автоматизации можно использовать config-monitor + git webhook compile 'org.springframework.cloud:spring-cloud-config-monitor' 3. Не забывать @RefreshScope -
spring-cloud-config-monitor Config Server Экзаминатор Экзаминатор GitHub commit Config Monitor webhook
Queue refresh event Теология Теология Теология ... auto /refresh on event -
Физика Математика Теология Теология Queue Экзаминатор Config Server ALL APP
Discovery Server ALL APP ALL APP -
Физика Математика Экзаминатор Экзаминатор Теология Теология Queue Config Server ALL
APP Discovery Server ALL APP ALL APP -
Физика Математика Экзаминатор Экзаминатор Теология Теология Queue Config Server ALL
APP Discovery Server ALL APP ALL APP Экзаминатор -
Физика Математика Экзаминатор Экзаминатор Теология Теология Gateway Server Queue Config
Server ALL APP Discovery Server ALL APP ALL APP -
283 Хочу бегемота! Rent Service Payment Service Security Service Blockchain
Service Insurance Service 5мс -
284 Хочу бегемота! Rent Service Payment Service Security Service Blockchain
Service Insurance Service 300мс -
285 Хочу бегемота! Rent Service Payment Service Security Service Blockchain
Service Insurance Service 300мс -
286 Хочу бегемота! Rent Service Payment Service Security Service Blockchain
Service Insurance Service 300мс -
287 Хочу бегемота! Rent Service Payment Service Security Service Blockchain
Service Insurance Service 300мс -
289 Хочу бегемота! Rent Service Payment Service Security Service Blockchain
Service Insurance Service 300мс -
290 Хочу бегемота! Rent Service Payment Service Security Service Blockchain
Service Insurance Service 300мс Open -
291 Хочу бегемота! Rent Service Payment Service Security Service Blockchain
Service Insurance Service 300мс Half-Open -
292 Хочу бегемота! Rent Service Payment Service Security Service Blockchain
Service Insurance Service 5мс Half-Open -
293 Хочу бегемота! Rent Service Payment Service Security Service Blockchain
Service Insurance Service 5мс -
Gateway + Hystrix + Dashboard + Discovery dependencies { compile
'org.springframework.boot:spring-boot-starter-web' compile 'org.springframework.boot:spring-boot-starter-actuator' compile 'org.springframework.cloud:spring-cloud-starter-zuul' compile 'org.springframework.cloud:spring-cloud-starter-eureka' compile 'org.springframework.cloud:spring-cloud-starter-hystrix' compile 'org.springframework.cloud:spring-cloud-starter-hystrix-dashboard' } -
302 Rent Service Payment Service No TraceId No SpanId TraceId
= X SpanId = A TraceId = X SpanId = B TraceId = X SpanId = C -
303 Rent Service Payment Service Blockchain Service No TraceId No
SpanId TraceId = X SpanId = A TraceId = X SpanId = B TraceId = X SpanId = C TraceId = X SpanId = D TraceId = X SpanId = D TraceId = X SpanId = F -
304 Rent Service Payment Service Security Service Blockchain Service No
TraceId No SpanId TraceId = X SpanId = A TraceId = X SpanId = B TraceId = X SpanId = C TraceId = X SpanId = D TraceId = X SpanId = D TraceId = X SpanId = E TraceId = X SpanId = E TraceId = X SpanId = F TraceId = X SpanId = G -
305 Rent Service Payment Service Security Service Blockchain Service No
TraceId No SpanId TraceId = X SpanId = A TraceId = X SpanId = B TraceId = X SpanId = B TraceId = X SpanId = C TraceId = X SpanId = C TraceId = X SpanId = D TraceId = X SpanId = D TraceId = X SpanId = E TraceId = X SpanId = E TraceId = X SpanId = F TraceId = X SpanId = G -
306 Rent Service Payment Service Security Service Blockchain Service TraceId
= X SpanId = A No TraceId No SpanId TraceId = X SpanId = A TraceId = X SpanId = A TraceId = X SpanId = B TraceId = X SpanId = B TraceId = X SpanId = C TraceId = X SpanId = C TraceId = X SpanId = D TraceId = X SpanId = D TraceId = X SpanId = E TraceId = X SpanId = E TraceId = X SpanId = F TraceId = X SpanId = G -
310 Rent Service Payment Service SpanId = B Client Send
TraceId = X SpanId = A -
311 Rent Service Payment Service SpanId = B Client Send
SpanId = B Server Received TraceId = X SpanId = A TraceId = X SpanId = C -
312 Rent Service Payment Service SpanId = B Client Send
SpanId = B Server Received SpanId = B Server Send TraceId = X SpanId = A TraceId = X SpanId = C -
313 Rent Service Payment Service SpanId = B Client Send
SpanId = B Server Received SpanId = B Client Received SpanId = B Server Send TraceId = X SpanId = A TraceId = X SpanId = C -
Not smart = This is main documentation This document describes
how to be the most fundamental and important document in the world of documents ... COPY-PASTE documentation from another document ... 315 -
Not so smart = This is main documentation This document
describes how to be the most fundamental and important document in the world of documents include::https://raw.github.com/asciidoctor/asciidoctor/master/Gemfile[] include::../other.adoc[] include::/home/tolkv/git/docs-0/superdoc.adoc[] 316 -
Really smart = This is main documentation This document describes
how to be the most fundamental and important document in the world of documents include::https://raw.github.com/asciidoctor/asciidoctor/master/Gemfile[] include::gradle://gradle-advanced:service-with-deps:1.0/deps.adoc[] include::gradle://:service/doc.adoc[] 317 -
Payment Service[jar,doc] Insurance Service [jar,doc] One Point of View [UberDoc.zip]
Rent Service[jar,doc] Other Service [jar,doc] Агрегация информации 318 -
Парадокс централизации Чтобы эффективно разрабатывать распределённые приложения, нам нужны очень
хорошие централизованные библиотеки и инструменты Например: логирование, health-checking, метрики, обработка типовых ошибок, автодокументирование 319 -
Парадокс централизации Чтобы эффективно разрабатывать распределённые приложения, нам нужны очень
хорошие централизованные библиотеки и инструменты Но: не выносите бизнес-логику или доменные объекты! Не размывайте бизнес-контекст вашего API 320