참고사이트 : https://jongmin92.github.io/2019/03/31/Java/java-async-1/
https://moonscode.tistory.com/123
비동기
logging.level.org.springframework.web=DEBUG
package com.example.demo.Asynchronous;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadLocalRandom;
@RestController
public class HelloWorldCOntroller {
@GetMapping
public Callable<String> hello(){
return () -> {
Thread.sleep(ThreadLocalRandom.current().nextInt(5000));
return "Hello World";
};
}
}
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.4.RELEASE)
2020-02-07 01:14:53.579 INFO 2988 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication on DESKTOP-6HPEM1U with PID 2988 (D:\example\springboot2_mvc_example2\target\classes started by k in D:\example\springboot2_mvc_example2)
2020-02-07 01:14:53.582 INFO 2988 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to default profiles: default
2020-02-07 01:14:54.334 INFO 2988 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2020-02-07 01:14:54.340 INFO 2988 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2020-02-07 01:14:54.341 INFO 2988 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.30]
2020-02-07 01:14:54.389 INFO 2988 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2020-02-07 01:14:54.390 DEBUG 2988 --- [ main] o.s.web.context.ContextLoader : Published root WebApplicationContext as ServletContext attribute with name [org.springframework.web.context.WebApplicationContext.ROOT]
2020-02-07 01:14:54.390 INFO 2988 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 772 ms
2020-02-07 01:14:54.488 INFO 2988 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-02-07 01:14:54.493 DEBUG 2988 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : ControllerAdvice beans: 0 @ModelAttribute, 0 @InitBinder, 1 RequestBodyAdvice, 1 ResponseBodyAdvice
2020-02-07 01:14:54.525 DEBUG 2988 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : 3 mappings in 'requestMappingHandlerMapping'
2020-02-07 01:14:54.549 DEBUG 2988 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Patterns [/webjars/**, /**] in 'resourceHandlerMapping'
2020-02-07 01:14:54.556 DEBUG 2988 --- [ main] .m.m.a.ExceptionHandlerExceptionResolver : ControllerAdvice beans: 0 @ExceptionHandler, 1 ResponseBodyAdvice
2020-02-07 01:14:54.572 WARN 2988 --- [ main] ion$DefaultTemplateResolverConfiguration : Cannot find template location: classpath:/templates/ (please add some templates or check your Thymeleaf configuration)
2020-02-07 01:14:54.665 INFO 2988 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2020-02-07 01:14:54.669 INFO 2988 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 1.33 seconds (JVM running for 1.925)
2020-02-07 01:15:10.437 INFO 2988 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-02-07 01:15:10.438 INFO 2988 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020-02-07 01:15:10.438 DEBUG 2988 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Detected StandardServletMultipartResolver
2020-02-07 01:15:10.442 DEBUG 2988 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data
2020-02-07 01:15:10.442 INFO 2988 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 4 ms
2020-02-07 01:15:10.447 DEBUG 2988 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : GET "/", parameters={}
2020-02-07 01:15:10.452 DEBUG 2988 --- [nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.Asynchronous.HelloWorldCOntroller#hello()
2020-02-07 01:15:10.460 DEBUG 2988 --- [nio-8080-exec-1] o.s.w.c.request.async.WebAsyncManager : Started async request
2020-02-07 01:15:10.461 DEBUG 2988 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Exiting but response remains open for further handling
2020-02-07 01:15:13.377 DEBUG 2988 --- [ task-1] o.s.w.c.request.async.WebAsyncManager : Async result set, dispatch to /
2020-02-07 01:15:13.381 DEBUG 2988 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : "ASYNC" dispatch for GET "/", parameters={}
2020-02-07 01:15:13.382 DEBUG 2988 --- [nio-8080-exec-2] s.w.s.m.m.a.RequestMappingHandlerAdapter : Resume with async result ["Hello World"]
2020-02-07 01:15:13.390 DEBUG 2988 --- [nio-8080-exec-2] m.m.a.RequestResponseBodyMethodProcessor : Using 'text/html', given [text/html, application/xhtml+xml, image/webp, image/apng, application/signed-exchange;v=b3, application/xml;q=0.9, */*;q=0.8] and supported [text/plain, */*, text/plain, */*, application/json, application/*+json, application/json, application/*+json]
2020-02-07 01:15:13.390 DEBUG 2988 --- [nio-8080-exec-2] m.m.a.RequestResponseBodyMethodProcessor : Writing ["Hello World"]
2020-02-07 01:15:13.394 DEBUG 2988 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Exiting from "ASYNC" dispatch, status 200
ResponseBodyEmitter를 사용해서 응답을 수집하고 클라이언트에 전달하기
package com.example.demo.Asynchronous;
import java.math.BigDecimal;
public class Order {
private String id;
private BigDecimal amount;
public Order() {
}
public Order(String id, BigDecimal amount) {
this.id = id;
this.amount = amount;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
@Override
public String toString() {
return "Order{" +
"id='" + id + '\'' +
", amount=" + amount +
'}';
}
}
package com.example.demo.Asynchronous;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
@Service
public class OrderService {
private final List<Order> orders = new ArrayList<>();
@PostConstruct
public void setup(){
createOrders();
}
public Iterable<Order> findAll(){
return List.copyOf(orders);
}
private Iterable<Order> createOrders(){
for(int i=0; i<25; i++){
this.orders.add(createOrder());
}
return orders;
}
private Order createOrder() {
String id = UUID.randomUUID().toString();
double amount = ThreadLocalRandom.current().nextDouble(1000.00d);
return new Order(id, BigDecimal.valueOf(amount));
}
}
package com.example.demo.Asynchronous;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import java.io.IOException;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
@RestController
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/orders")
public ResponseBodyEmitter hello(){
var emitter = new ResponseBodyEmitter();
var executor = Executors.newSingleThreadExecutor();
executor.execute(()->{
var orders = orderService.findAll();
try{
for(var order : orders){
randomDelay();
emitter.send(order);
}
emitter.complete();
} catch (IOException e){
emitter.completeWithError(e);
}
});
executor.shutdown();
return emitter;
}
private void randomDelay() {
try{
Thread.sleep(ThreadLocalRandom.current().nextInt(150));
} catch (InterruptedException e){
Thread.currentThread().interrupt();
}
}
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | C:\Users\k>curl -v http://localhost:8080/orders * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 8080 (#0) > GET /orders HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.55.1 > Accept: */* > < HTTP/1.1 200 < Transfer-Encoding: chunked < Date: Thu, 06 Feb 2020 16:39:24 GMT < {"id":"f4e6836a-44e6-471a-bd23-48660c5bede1","amount":838.352665366575}{"id":"545c0c71-db06-4654-a7e9-b5c1a38e5911","amount":186.37894898194028} {"id":"f0309c77-37b9-4dc3-883b-4dc7d049e837","amount":830.1916746885163}{"id":"d4360dde-cb91-4161-8812-dccb4639720c","amount":260.42120067070584} {"id":"493b8ad7-5aa7-4dd3-8cb2-07fdae02582b","amount":665.3502781777786}{"id":"bb538070-4001-4e79-a92b-b653dc282b8e","amount":578.1831659999962} {"id":"4397420f-ff4f-4c51-87dd-4176039c80a7","amount":317.0322435813805}{"id":"58c46c07-a28e-4182-acef-7d1a455dfd57","amount":755.9804485675882} {"id":"a949a492-8583-48a1-befb-3ea45fa38a55","amount":39.89508088426419}{"id":"4129b327-4ae7-4ba7-b4a6-82313f684289","amount":977.5548380796984} {"id":"338fce90-683d-4b7d-9b07-711a4c048e1e","amount":607.8094883472315}{"id":"d77f4b61-2045-4a15-afdd-c411a16b967a","amount":313.109315479815} {"id":"8595ccad-0511-4770-8778-d958835580ff","amount":325.7668631365621}{"id":"807b1a10-817f-4506-bbfc-80d9f95e0048","amount":163.56409054478917} {"id":"c27a794b-8e24-4cd0-8919-3eb85523393d","amount":871.3926916988252}{"id":"6309f791-a360-445e-a162-7f5777f71713","amount":832.9733279155695} {"id":"1e7ec31a-917b-47e7-8224-f2dedaa02bf8","amount":65.77336945863554}{"id":"dad250bc-6210-45ad-a9b0-b9cb0c75d081","amount":196.04229024882414} {"id":"4b4c8c59-1b6e-4017-99af-60ed945a2bca","amount":290.24664329899053}{"id":"7b5ae41d-bf27-47dd-ac7e-dc1b26c97694","amount":107.34350811184478} {"id":"ab8f3994-feea-4fc2-a6ae-b6fc7a42916f","amount":434.1122832448087}{"id":"6868a692-c61b-432e-8605-e2a74da399b4","amount":234.60378343416588} {"id":"08279c57-6529-4c86-aed2-f3b8f772ab6e","amount":734.4986071006585}{"id":"2f45cd8f-6dbd-43df-85b2-be251207d852","amount":557.0776284866245} {"id":"5fcd429e-39a0-4eb9-be24-af434734c7fa","amount":214.67525662244137} * Connection #0 to host localhost left intact | cs |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.2.4.RELEASE) 2020-02-07 01:38:51.777 INFO 10132 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication on DESKTOP-6HPEM1U with PID 10132 (D:\example\springboot2_mvc_example2\target\classes started by k in D:\example\springboot2_mvc_example2) 2020-02-07 01:38:51.780 INFO 10132 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to default profiles: default 2020-02-07 01:38:52.528 INFO 10132 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2020-02-07 01:38:52.534 INFO 10132 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2020-02-07 01:38:52.534 INFO 10132 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.30] 2020-02-07 01:38:52.592 INFO 10132 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2020-02-07 01:38:52.592 DEBUG 10132 --- [ main] o.s.web.context.ContextLoader : Published root WebApplicationContext as ServletContext attribute with name [org.springframework.web.context.WebApplicationContext.ROOT] 2020-02-07 01:38:52.592 INFO 10132 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 778 ms 2020-02-07 01:38:52.711 INFO 10132 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' 2020-02-07 01:38:52.717 DEBUG 10132 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : ControllerAdvice beans: 0 @ModelAttribute, 0 @InitBinder, 1 RequestBodyAdvice, 1 ResponseBodyAdvice 2020-02-07 01:38:52.754 DEBUG 10132 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : 3 mappings in 'requestMappingHandlerMapping' 2020-02-07 01:38:52.767 DEBUG 10132 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Patterns [/webjars/**, /**] in 'resourceHandlerMapping' 2020-02-07 01:38:52.773 DEBUG 10132 --- [ main] .m.m.a.ExceptionHandlerExceptionResolver : ControllerAdvice beans: 0 @ExceptionHandler, 1 ResponseBodyAdvice 2020-02-07 01:38:52.785 WARN 10132 --- [ main] ion$DefaultTemplateResolverConfiguration : Cannot find template location: classpath:/templates/ (please add some templates or check your Thymeleaf configuration) 2020-02-07 01:38:52.857 INFO 10132 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2020-02-07 01:38:52.860 INFO 10132 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 1.343 seconds (JVM running for 2.043) 2020-02-07 01:39:23.981 INFO 10132 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' 2020-02-07 01:39:23.982 INFO 10132 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' 2020-02-07 01:39:23.982 DEBUG 10132 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Detected StandardServletMultipartResolver 2020-02-07 01:39:23.988 DEBUG 10132 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data 2020-02-07 01:39:23.988 INFO 10132 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 6 ms 2020-02-07 01:39:23.992 DEBUG 10132 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : GET "/orders", parameters={} 2020-02-07 01:39:23.994 DEBUG 10132 --- [nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.Asynchronous.OrderController#hello() 2020-02-07 01:39:24.007 DEBUG 10132 --- [nio-8080-exec-1] o.s.w.c.request.async.WebAsyncManager : Started async request 2020-02-07 01:39:24.048 DEBUG 10132 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Exiting but response remains open for further handling 2020-02-07 01:39:25.878 DEBUG 10132 --- [pool-1-thread-1] o.s.w.c.request.async.WebAsyncManager : Async result set, dispatch to /orders 2020-02-07 01:39:25.882 DEBUG 10132 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : "ASYNC" dispatch for GET "/orders", parameters={} 2020-02-07 01:39:25.883 DEBUG 10132 --- [nio-8080-exec-2] s.w.s.m.m.a.RequestMappingHandlerAdapter : Resume with async result [] 2020-02-07 01:39:25.884 DEBUG 10132 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Exiting from "ASYNC" dispatch, status 200 | cs |
ResponseBodyEmitter가 만들어진 뒤 맨 마지막에 반환된다.
이 호출의 모든 결과는 ResponseBodyEmitter의 send 메소드를 사용해서 하나씩 반환된다. 모든 객체가 전달되면, complete() 메소드가 호출돼야 응답을 전달할 스레드에서 요청을 완료하고 다음 응답을 처리하기 위해 반환된다.
Mock 테스트
package com.example.demo;
import com.example.demo.Asynchronous.Order;
import com.example.demo.Asynchronous.OrderService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import java.math.BigDecimal;
import java.util.List;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
public class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private OrderService orderService;
@Test
public void foo() throws Exception{
when(orderService.findAll())
.thenReturn(List.of(new Order("123456", BigDecimal.TEN)));
MvcResult mvcResult = mockMvc.perform(get("/orders"))
.andExpect(request().asyncStarted())
.andDo(MockMvcResultHandlers.log())
.andReturn();
mockMvc.perform(asyncDispatch(mvcResult))
.andDo(MockMvcResultHandlers.log())
.andExpect(status().isOk())
.andExpect(content().json("{\n" +
" \"id\":\"123456\",\n" +
" \"amount\":10\n" +
"}"));
}
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | 02:26:20.534 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating CacheAwareContextLoaderDelegate from class [org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate] 02:26:20.546 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating BootstrapContext using constructor [public org.springframework.test.context.support.DefaultBootstrapContext(java.lang.Class,org.springframework.test.context.CacheAwareContextLoaderDelegate)] 02:26:20.576 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating TestContextBootstrapper for test class [com.example.demo.OrderControllerTest] from class [org.springframework.boot.test.context.SpringBootTestContextBootstrapper] 02:26:20.591 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Neither @ContextConfiguration nor @ContextHierarchy found for test class [com.example.demo.OrderControllerTest], using SpringBootContextLoader 02:26:20.594 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Did not detect default resource location for test class [com.example.demo.OrderControllerTest]: class path resource [com/example/demo/OrderControllerTest-context.xml] does not exist 02:26:20.594 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Did not detect default resource location for test class [com.example.demo.OrderControllerTest]: class path resource [com/example/demo/OrderControllerTestContext.groovy] does not exist 02:26:20.595 [main] INFO org.springframework.test.context.support.AbstractContextLoader - Could not detect default resource locations for test class [com.example.demo.OrderControllerTest]: no resource found for suffixes {-context.xml, Context.groovy}. 02:26:20.596 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils - Could not detect default configuration classes for test class [com.example.demo.OrderControllerTest]: OrderControllerTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration. 02:26:20.645 [main] DEBUG org.springframework.test.context.support.ActiveProfilesUtils - Could not find an 'annotation declaring class' for annotation type [org.springframework.test.context.ActiveProfiles] and class [com.example.demo.OrderControllerTest] 02:26:20.703 [main] DEBUG org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider - Identified candidate component class: file [D:\example\springboot2_mvc_example2\target\classes\com\example\demo\DemoApplication.class] 02:26:20.704 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Found @SpringBootConfiguration com.example.demo.DemoApplication for test class com.example.demo.OrderControllerTest 02:26:20.811 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - @TestExecutionListeners is not present for class [com.example.demo.OrderControllerTest]: using defaults. 02:26:20.811 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener, org.springframework.test.context.web.ServletTestExecutionListener, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener, org.springframework.test.context.support.DependencyInjectionTestExecutionListener, org.springframework.test.context.support.DirtiesContextTestExecutionListener, org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener, org.springframework.test.context.event.EventPublishingTestExecutionListener] 02:26:20.821 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Skipping candidate TestExecutionListener [org.springframework.test.context.transaction.TransactionalTestExecutionListener] due to a missing dependency. Specify custom listener classes or make the default listener classes and their required dependencies available. Offending class: [org/springframework/transaction/TransactionDefinition] 02:26:20.822 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Skipping candidate TestExecutionListener [org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener] due to a missing dependency. Specify custom listener classes or make the default listener classes and their required dependencies available. Offending class: [org/springframework/transaction/interceptor/TransactionAttribute] 02:26:20.822 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Using TestExecutionListeners: [org.springframework.test.context.web.ServletTestExecutionListener@5023bb8b, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener@5d5f10b2, org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener@74c79fa2, org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExecutionListener@1e0f9063, org.springframework.test.context.support.DirtiesContextTestExecutionListener@53bd8fca, org.springframework.test.context.event.EventPublishingTestExecutionListener@7642df8f, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener@3e30646a, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener@5cde6747, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener@63a270c9, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener@37c7595, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener@3ed242a4] 02:26:20.826 [main] DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - Before test class: context [DefaultTestContext@52851b44 testClass = OrderControllerTest, testInstance = [null], testMethod = [null], testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@584f54e6 testClass = OrderControllerTest, locations = '{}', classes = '{class com.example.demo.DemoApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[[ImportsContextCustomizer@5d8bafa9 key = [org.springframework.boot.test.autoconfigure.web.servlet.MockMvcAutoConfiguration, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcWebClientAutoConfiguration, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcWebDriverAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcSecurityConfiguration]], org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@6933b6c6, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@626abbd0, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@420b8b1d, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@2fb0623e, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@4b3fa0b3, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@24105dc5], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true]], class annotated with @DirtiesContext [false] with mode [null]. 02:26:20.872 [main] DEBUG org.springframework.test.context.support.TestPropertySourceUtils - Adding inlined properties to environment: {spring.jmx.enabled=false, org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true, server.port=-1} . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.2.4.RELEASE) 2020-02-07 02:26:21.336 INFO 7444 --- [ main] com.example.demo.OrderControllerTest : Starting OrderControllerTest on DESKTOP-6HPEM1U with PID 7444 (started by k in D:\example\springboot2_mvc_example2) 2020-02-07 02:26:21.338 INFO 7444 --- [ main] com.example.demo.OrderControllerTest : No active profile set, falling back to default profiles: default 2020-02-07 02:26:21.377 DEBUG 7444 --- [ main] o.s.w.c.s.GenericWebApplicationContext : Refreshing org.springframework.web.context.support.GenericWebApplicationContext@60b71e8f 2020-02-07 02:26:23.415 INFO 7444 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' 2020-02-07 02:26:23.435 DEBUG 7444 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : ControllerAdvice beans: 0 @ModelAttribute, 0 @InitBinder, 1 RequestBodyAdvice, 1 ResponseBodyAdvice 2020-02-07 02:26:23.535 DEBUG 7444 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : 3 mappings in 'requestMappingHandlerMapping' 2020-02-07 02:26:23.610 DEBUG 7444 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Patterns [/webjars/**, /**] in 'resourceHandlerMapping' 2020-02-07 02:26:23.629 DEBUG 7444 --- [ main] .m.m.a.ExceptionHandlerExceptionResolver : ControllerAdvice beans: 0 @ExceptionHandler, 1 ResponseBodyAdvice 2020-02-07 02:26:23.685 WARN 7444 --- [ main] ion$DefaultTemplateResolverConfiguration : Cannot find template location: classpath:/templates/ (please add some templates or check your Thymeleaf configuration) 2020-02-07 02:26:23.825 INFO 7444 --- [ main] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet '' 2020-02-07 02:26:23.825 INFO 7444 --- [ main] o.s.t.web.servlet.TestDispatcherServlet : Initializing Servlet '' 2020-02-07 02:26:23.845 INFO 7444 --- [ main] o.s.t.web.servlet.TestDispatcherServlet : Completed initialization in 20 ms 2020-02-07 02:26:23.882 INFO 7444 --- [ main] com.example.demo.OrderControllerTest : Started OrderControllerTest in 2.978 seconds (JVM running for 4.139) 2020-02-07 02:26:23.990 DEBUG 7444 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.Asynchronous.OrderController#orders() 2020-02-07 02:26:24.016 DEBUG 7444 --- [ main] o.s.w.c.request.async.WebAsyncManager : Started async request 2020-02-07 02:26:24.193 DEBUG 7444 --- [pool-1-thread-1] o.s.w.c.request.async.WebAsyncManager : Async result set, dispatch to /orders 2020-02-07 02:26:24.198 DEBUG 7444 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Resume with async result [] 2020-02-07 02:26:24.231 DEBUG 7444 --- [extShutdownHook] o.s.w.c.s.GenericWebApplicationContext : Closing org.springframework.web.context.support.GenericWebApplicationContext@60b71e8f, started on Fri Feb 07 02:26:21 KST 2020 2020-02-07 02:26:24.234 INFO 7444 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor' Process finished with exit code 0 | cs |
Emitter
서버-전달-이벤트에 허용되는 항목
id : 이벤트 ID
event : 이벤트 유형
data : 이벤트 데이터
retry : 이벤트 스트림에 다시 연결되는 시간
@GetMapping("/orders")
public SseEmitter orders(){
SseEmitter emitter = new SseEmitter();
var executor = Executors.newSingleThreadExecutor();
executor.execute(()->{
var orders = orderService.findAll();
try{
for(var order : orders){
randomDelay();
var eventBuilder = event();
emitter.send(
eventBuilder
.data(order)
.name("order-created")
.id(String.valueOf(order.hashCode()))
);
}
emitter.complete();
} catch (IOException e){
emitter.completeWithError(e);
}
});
executor.shutdown();
return emitter;
}
웹 소켓
WebSocketHandler 메소드
afterConnection : 웹 소켓 접속이 열리고 사용 준비가 될 때 호출된다.
handleMessage ; 웹 소켓 메시지가 핸들러에 도착했을 떄 호출된다.
handleTransportError : 오류가 발생할 때 호출된다.
afterConnectionClose : 웹 소켓 접속이 종료된 후 호출된다.
supportsPartiaMessages : 핸들러가 부분 메시지를 지원할 경우, true로 설정하면 웹 소켓 메시지가 여러 호출을 통해 도착할 수 있다.
STOMP 와 웹 소켓
@MessageMapping
Message : 헤더와 바디를 포함한 실제 기본 메시지
@Payload : 메시지의 페이로드(기본값), 인수는 검증을 위해 @Validated 애노테이션을 추가할 수 있다.
@Header : 메시지에 주어진 헤더 가져오기
@Headers : 메시지의 모든 헤더를 맵 인수로 가져온다
MessageHeaders : 모든 메시지 헤더
Principal : 설정된 경우 현재 사용자
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | C:\Users\k>curl -v http://localhost:8080/orders * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 8080 (#0) > GET /orders HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.55.1 > Accept: */* > < HTTP/1.1 200 < Content-Type: text/event-stream;charset=UTF-8 < Transfer-Encoding: chunked < Date: Fri, 07 Feb 2020 07:21:16 GMT < data:{"id":"31e7bb40-80f4-411d-ae80-8564ee8d85ef","amount":331.214684413747} event:order-created id:244344244 data:{"id":"18845698-c2a4-432e-868b-95242b112a26","amount":391.75748833143007} event:order-created id:970429171 data:{"id":"6f7484fb-8f42-48ae-bd02-0389d8e2b032","amount":415.7025584316099} event:order-created id:505767684 data:{"id":"2fa18563-521a-498d-91c6-bc471ae3839b","amount":623.8232943915266} event:order-created id:324841298 data:{"id":"48a5d3fb-977a-42ee-b022-ab07095467c5","amount":224.19822760245657} event:order-created id:2080631628 data:{"id":"08c46338-f8ea-4c9c-9588-c98a90ab0552","amount":317.4306937939991} event:order-created id:1573505209 data:{"id":"2f42bb8c-c596-433c-9d3d-adf8b4411880","amount":295.9665450679093} event:order-created id:1818173067 data:{"id":"61e439f9-9929-4673-80a8-f47f86030f53","amount":207.7535481600572} event:order-created id:706567566 data:{"id":"123fec7a-a849-4634-b744-f4e155597ad6","amount":969.963021875877} event:order-created id:617833937 data:{"id":"27825f52-3b60-4960-bcb9-76bd8c08f7a7","amount":496.3000323143556} event:order-created id:2025255530 data:{"id":"5634b87f-24e3-4f0f-8a93-4c8f17e51b42","amount":28.181733114026496} event:order-created id:1312499224 data:{"id":"32ed8977-fd57-4502-bb2e-0ba88869a685","amount":526.2572793299545} event:order-created id:1587676234 data:{"id":"a97f22bd-aa4f-4c29-91b5-78af03dbc512","amount":671.7660999127987} event:order-created id:1549550038 data:{"id":"58dddb21-b0ca-46df-b83f-a7aafe3c865c","amount":23.885982145928743} event:order-created id:1386286678 data:{"id":"85b49752-bea1-476a-9b06-af9accde9ff9","amount":699.3201635431369} event:order-created id:1301350608 data:{"id":"0188aa88-8084-44cb-b019-7c9bccde4091","amount":229.8101862050299} event:order-created id:168083627 data:{"id":"3ebee71f-f12c-4757-b39d-7967d9b7df3d","amount":322.8930912320822} event:order-created id:1838019477 data:{"id":"7c81943c-c556-44a0-99f7-7698563ec4db","amount":556.8998996450105} event:order-created id:1058749295 data:{"id":"2a5e5990-e03b-420c-8de6-4d12c26fd36e","amount":506.8906685307027} event:order-created id:353051992 data:{"id":"c8d967ab-4cb3-4679-8e14-a3d5d0185d98","amount":192.46240045767294} event:order-created id:1618868643 data:{"id":"88fba70a-5e77-4344-9e5e-5055926741c7","amount":738.824711395208} event:order-created id:799157140 data:{"id":"741ef8b5-a4c7-444d-a398-9e5456dc6013","amount":302.4123355658356} event:order-created id:1529767280 data:{"id":"4ca58181-9713-4978-87a9-190b6555cff5","amount":523.1790703189656} event:order-created id:1942849067 data:{"id":"3f661cc9-57b2-4cd0-84c5-728c81f62846","amount":164.2079768093171} event:order-created id:1667010897 data:{"id":"e0598d4b-a276-4804-8272-a38d5b911a3a","amount":638.9018434626661} event:order-created id:340110894 * Connection #0 to host localhost left intact | cs |
'WEB > 스프링 부트 2' 카테고리의 다른 글
WebFlux + Thymeleaf (0) | 2020.02.07 |
---|---|
WebFlux (0) | 2020.02.07 |
Jetty SSL, Http to Https (0) | 2020.02.06 |
Spring SSL, Http to Https (0) | 2020.02.05 |
Messages.properties (0) | 2020.02.05 |