Spring Web MVC에는 요청을 라우팅하고 처리하는 데 기능이 사용되는 경량 기능 프로그래밍 모델인 WebMvc.fn이 포함되어 있으며 contracts은 불변성을 위해 설계되었습니다. annotation 기반 프로그래밍 모델의 대안이지만 동일한 DispatcherServlet에서 실행됩니다.
WebMvc.fn에서 HTTP 요청은 HandlerFunction으로 처리됩니다. 이 함수는 ServerRequest를 받고 ServerResponse를 반환하는 함수입니다. 요청과 응답 객체에는 HTTP 요청 및 응답에 대한 JDK 8 친화적인 액세스를 제공하는 변경 불가능한 contracts이 있습니다. HandlerFunction은 어노테이션 기반 프로그래밍 모델에서 @RequestMapping 메소드의 바디에 해당합니다.
들어오는 요청은 RouterFunction이 있는 핸들러 함수로 라우팅됩니다. 이 함수는 ServerRequest를 사용하고 선택적 HandlerFunction(즉, Optional<HandlerFunction>)을 반환하는 함수입니다. 라우터 함수가 일치하면 핸들러 함수가 반환됩니다. 그렇지 않으면 비어 있는 Optional. RouterFunction은 @RequestMapping annotation과 동일하지만 라우터 기능이 데이터뿐만 아니라 동작도 제공한다는 주요 차이점이 있습니다.
RouterFunctions.route()는 다음 예제와 같이 라우터 생성을 용이하게 하는 라우터 빌더를 제공합니다.
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
import static org.springframework.web.servlet.function.RouterFunctions.route;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
public class PersonHandler {
// ...
public ServerResponse listPeople(ServerRequest request) {
// ...
}
public ServerResponse createPerson(ServerRequest request) {
// ...
}
public ServerResponse getPerson(ServerRequest request) {
// ...
}
}
예를 들어 @Configuration 클래스에 노출하여 RouterFunction을 빈으로 등록하면 Running a Server에서 설명한 대로 서블릿에 의해 자동 감지됩니다.
ServerRequest 및 ServerResponse는 헤더, 본문, 메서드 및 상태 코드를 포함하여 HTTP 요청 및 응답에 대한 JDK 8 친화적인 액세스를 제공하는 변경할 수 없는 인터페이스입니다.
ServerRequest는 HTTP 메서드, URI, 헤더 및 쿼리 매개변수에 대한 액세스를 제공하는 반면 본문에 대한 액세스는 본문 메서드를 통해 제공됩니다.
다음 예제에서는 요청 본문을 문자열로 추출합니다.
String string = request.body(String.class);
다음 예제에서는 본문을 List<Person>으로 추출합니다. 여기서 Person객체는 JSON 또는 XML과 같은 직렬화된 형식에서 디코딩됩니다.
List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});
다음 예에서는 매개변수에 액세스하는 방법을 보여줍니다.
MultiValueMap<String, String> params = request.params();
ServerResponse는 HTTP 응답에 대한 액세스를 제공하며 변경할 수 없으므로 빌드 메서드를 사용하여 생성할 수 있습니다. 빌더를 사용하여 응답 상태를 설정하거나 응답 헤더를 추가하거나 본문을 제공할 수 있습니다. 다음 예에서는 JSON 콘텐츠가 포함된 200(OK) 응답을 생성합니다.
Person person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
다음 예는 Location 헤더가 있고 본문이 없는 201(CREATED) 응답을 작성하는 방법을 보여줍니다.
URI location = ...
ServerResponse.created(location).build();
CompletableFuture, Publisher 또는 ReactiveAdapterRegistry에서 지원하는 기타 유형의 형식으로 비동기 결과를 본문으로 사용할 수도 있습니다. 예를 들어:
Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
본문뿐만 아니라 상태 또는 헤더도 비동기식 유형을 기반으로 하는 경우, ServerResponse에서 정적 비동기 메서드를 사용할 수 있습니다. CompletableFuture<ServerResponse>, Publisher<ServerResponse> 또는 ReactiveAdapterRegistry에서 지원하는 기타 비동기식 유형을 허용하는 것들을 사용할 수 있습니다. 예를 들어:
Mono<ServerResponse> asyncResponse = webClient.get().retrieve().bodyToMono(Person.class)
.map(p -> ServerResponse.ok().header("Name", p.name()).body(p));
ServerResponse.async(asyncResponse);
Server-Sent Events는 ServerResponse의 정적 sse 메소드를 통해 제공될 수 있습니다. 해당 메소드에서 제공하는 빌더를 사용하면 문자열 또는 기타 객체를 JSON으로 보낼 수 있습니다. 예를 들어:
public RouterFunction<ServerResponse> sse() {
return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> {
// Save the sseBuilder object somewhere..
}));
}
// In some other thread, sending a String
sseBuilder.send("Hello world");
// Or an object, which will be transformed into JSON
Person person = ...
sseBuilder.send(person);
// Customize the event by using the other methods
sseBuilder.id("42")
.event("sse event")
.data(person);
// and done at some point
sseBuilder.complete();
다음 예제와 같이 핸들러 함수를 람다로 작성할 수 있습니다.
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().body("Hello World");
그것은 편리하지만 응용 프로그램에는 여러 기능이 필요하고 여러 인라인 람다가 지저분해질 수 있습니다. 따라서 annotation 기반 응용 프로그램에서 @Controller와 유사한 역할을 하는 핸들러 클래스로 관련 핸들러 기능을 그룹화하는 것이 유용합니다. 예를 들어, 다음 클래스는 reactive Person 리포지토리를 노출합니다.
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
public class PersonHandler {
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
public ServerResponse listPeople(ServerRequest request) { // (1)
List<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people);
}
public ServerResponse createPerson(ServerRequest request) throws Exception { // (2)
Person person = request.body(Person.class);
repository.savePerson(person);
return ok().build();
}
public ServerResponse getPerson(ServerRequest request) { // (3)
int personId = Integer.parseInt(request.pathVariable("id"));
Person person = repository.getPerson(personId);
if (person != null) {
return ok().contentType(APPLICATION_JSON).body(person);
}
else {
return ServerResponse.notFound().build();
}
}
}
(1) listPeople은 저장소에서 찾은 모든 Person 객체를 다음과 같이 반환하는 핸들러 함수입니다.
JSON.
(2) createPerson은 요청 본문에 포함된 새 Person을 저장하는 핸들러 함수입니다.
(3) getPerson은 id 경로로 식별되는 한 사람을 반환하는 핸들러 함수입니다.
변하기 쉬운. 저장소에서 해당 Person을 검색하고 JSON 응답을 생성합니다.
설립하다. 찾을 수 없으면 404 Not Found 응답을 반환합니다.
기능적 엔드포인트는 Spring의 validation facilities을 사용하여 요청 본문에 유효성 검사를 적용할 수 있습니다. 예를 들어 Person에 대한 사용자 정의 Spring Validator 구현이 주어지면:
public class PersonHandler {
private final Validator validator = new PersonValidator(); // (1)
// ...
public ServerResponse createPerson(ServerRequest request) {
Person person = request.body(Person.class);
validate(person); // (2)
repository.savePerson(person);
return ok().build();
}
private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw new ServerWebInputException(errors.toString()); // (3)
}
}
}
(1) Validator 인스턴스를 생성합니다.
(2) validation을 적용합니다.
(3) 400 응답에 해당하는 예외를 발생시킵니다.
핸들러는 LocalValidatorFactoryBean을 기반으로 하는 전역 Validator 인스턴스를 생성하고 주입하여 표준 빈 유효성 검사 API(JSR-303)를 사용할 수도 있습니다. Spring Validation를 참조하십시오.
라우터 함수는 요청을 해당 HandlerFunction으로 라우팅하는 데 사용됩니다. 일반적으로 라우터 기능을 직접 작성하지 않고 RouterFunctions 유틸리티 클래스의 메서드를 사용하여 만듭니다. RouterFunctions.route()(매개변수 없음)는 라우터 기능을 생성하기 위한 유창한 빌더를 제공하는 반면, RouterFunctions.route(RequestPredicate, HandlerFunction)는 라우터를 생성하는 직접적인 방법을 제공합니다.
일반적으로 route() 빌더는 찾기 어려운 정적 가져오기 없이 일반적인 매핑 시나리오에 대한 편리한 바로 가기를 제공하므로 사용하는 것이 좋습니다. 예를 들어, 라우터 기능 빌더는 GET 요청에 대한 매핑을 생성하기 위해 GET(String, HandlerFunction) 메소드를 제공합니다. 및 POST에 대한 POST(String, HandlerFunction).
HTTP 메서드 기반 매핑 외에도 라우트 빌더는 요청에 매핑할 때 추가 조건자를 도입하는 방법을 제공합니다. 각 HTTP 메서드에는 RequestPredicate를 매개변수로 사용하는 오버로드된 변형이 있으며 이를 통해 추가 제약 조건을 표현할 수 있습니다.
자신만의 RequestPredicate를 작성할 수 있지만 RequestPredicates 유틸리티 클래스는 요청 경로, HTTP 메서드, 콘텐츠 유형 등을 기반으로 일반적으로 사용되는 구현을 제공합니다. 다음 예제에서는 요청 조건자를 사용하여 Accept 헤더를 기반으로 제약 조건을 생성합니다.
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().body("Hello World")).build();
다음을 사용하여 여러 request predicates를 함께 작성할 수 있습니다.
RequestPredicate.and(RequestPredicate) — 둘 다 일치해야 합니다.
RequestPredicate.or(RequestPredicate) - 둘 다 일치할 수 있습니다.
RequestPredicates의 많은 predicates가 구성됩니다. 예를 들어 RequestPredicates.GET(String)은 RequestPredicates.method(HttpMethod)와 RequestPredicates.path(String)로 구성된다. 빌더가 내부적으로 RequestPredicates.GET을 사용하고 이를 accept predicate로 구성하므로 위에 표시된 예제에서도 두 개의 요청 predicates를 사용합니다.
라우터 기능은 순서대로 평가됩니다. 첫 번째 경로가 일치하지 않으면 두 번째 경로가 평가되는 식입니다. 따라서 일반적인 경로보다 더 구체적인 경로를 선언하는 것이 좋습니다. 이는 라우터 기능을 Spring Bean으로 등록할 때도 중요하지만 나중에 설명합니다. 이 동작은 "가장 구체적인" 컨트롤러 메서드가 자동으로 선택되는 annotation 기반 프로그래밍 모델과 다릅니다.
라우터 함수 빌더를 사용할 때 정의된 모든 경로는 build()에서 반환되는 하나의 RouterFunction으로 구성됩니다. 여러 라우터 기능을 함께 구성하는 다른 방법도 있습니다.
RouterFunctions.route() 빌더의 add(RouterFunction)
RouterFunction.and(RouterFunction)
RouterFunction.andRoute(RequestPredicate, HandlerFunction) - 중첩된 RouterFunctions.route()가 있는 RouterFunction.and()의 바로 가기.
다음 예는 4가지 경로의 구성을 보여줍니다.
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> otherRoute = ...
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) //(1)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople) //(2)
.POST("/person", handler::createPerson) //(3)
.add(otherRoute) //(4)
.build();
(1) JSON과 일치하는 Accept 헤더가 있는 GET /person/{id}는 PersonHandler.getPerson으로 라우팅됩니다.
(2) JSON과 일치하는 Accept 헤더가 있는 GET /person은 PersonHandler.listPeople으로 라우팅됩니다.
(3) 추가 predicates가 없는 POST /person은 PersonHandler.createPerson에 매핑됩니다.
(4) 그리고 otherRoute는 다른 곳에서 생성되고 빌드된 경로에 추가되는 라우터 기능입니다.
라우터 기능 그룹이 공유 경로와 같은 공유 predicate를 갖는 것은 일반적입니다. 위의 예에서 공유 조건자는 3개의 경로에서 사용되는 /person과 일치하는 경로 조건자가 됩니다. annotation을 사용할 때, /person에 매핑되는 유형 수준 @RequestMapping annotation을 사용하여 이 중복을 제거합니다. WebMvc.fn에서 경로 술어는 라우터 기능 빌더의 path 메소드를 통해 공유할 수 있습니다. 예를 들어 위 예제의 마지막 몇 줄은 중첩 경로를 사용하여 다음과 같이 개선할 수 있습니다.
RouterFunction<ServerResponse> route = route()
.path("/person", builder -> builder //(1)
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET(accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson))
.build();
(1) path의 두 번째 매개변수는 라우터 빌더를 사용하는 소비자입니다.
경로 기반 중첩이 가장 일반적이지만 빌더에서 nest 메소드를 사용하여 모든 종류의 predicate에 중첩할 수 있습니다. 위의 내용은 여전히 공유 Accept-header 술어의 형태로 일부 중복을 포함합니다. 다음과 같이 accept와 함께 nest 메소드를 사용하여 더 개선할 수 있습니다.
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST("/person", handler::createPerson))
.build();
일반적으로 요청을 처리하는 데 필요한 구성 요소를 선언하기 위해 Spring 구성을 사용하는 MVC 구성을 통해 DispatcherHandler 기반 설정에서 라우터 기능을 실행합니다. MVC Java 구성은 기능적 엔드포인트를 지원하기 위해 다음 인프라 구성 요소를 선언합니다.
RouterFunctionMapping: Spring 구성에서 하나 이상의 RouterFunction<?> 빈을 감지하고, orders tem, RouterFunction.andOther를 통해 결합하고, 구성된 결과 RouterFunction으로 요청을 라우팅합니다.
HandlerFunctionAdapter: DispatcherHandler가 요청에 매핑된 HandlerFunction을 호출할 수 있게 해주는 간단한 어댑터입니다.
앞의 구성 요소는 기능적 끝점이 DispatcherServlet 요청 처리 수명 주기에 맞도록 하고 선언된 경우 주석이 달린 컨트롤러와 (잠재적으로) 나란히 실행되도록 합니다. Spring Boot 웹 스타터가 기능적 엔드포인트를 활성화하는 방법이기도 합니다.
다음 예는 WebFlux Java 구성을 보여줍니다.
@Configuration
@EnableMvc
public class WebConfig implements WebMvcConfigurer {
@Bean
public RouterFunction<?> routerFunctionA() {
// ...
}
@Bean
public RouterFunction<?> routerFunctionB() {
// ...
}
// ...
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// configure message conversion...
}
@Override
public void addCorsMappings(CorsRegistry registry) {
// configure CORS...
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// configure view resolution for HTML rendering...
}
}
라우팅 기능 빌더에서 before, after 또는 filter 메소드를 사용하여 핸들러 기능을 필터링할 수 있습니다. annotation을 사용하면 @ControllerAdvice, ServletFilter 또는 둘 다를 사용하여 유사한 기능을 얻을 수 있습니다. 필터는 빌더가 구축한 모든 경로에 적용됩니다. 즉, 중첩 경로에 정의된 필터는 "최상위" 경로에 적용되지 않습니다. 예를 들어 다음 예를 고려하십시오.
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople)
.before(request -> ServerRequest.from(request) //(1)
.header("X-RequestHeader", "Value")
.build()))
.POST("/person", handler::createPerson))
.after((request, response) -> logResponse(response)) //(2)
.build();
before 필터는 두 개의 GET 경로에만 적용됩니다.After 필터는 중첩된 경로를 포함한 모든 경로에 적용됩니다.라우터 빌더의 filter 메서드는 HandlerFilterFunction을 사용합니다. 이 함수는 ServerRequest 및 HandlerFunction을 사용하고 ServerResponse를 반환하는 함수입니다. 핸들러 함수 매개변수는 체인의 다음 요소를 나타냅니다. 이것은 일반적으로 라우팅되는 처리기이지만 여러 필터가 적용된 경우 다른 필터가 될 수도 있습니다.
이제 특정 경로가 허용되는지 여부를 결정할 수 있는 SecurityManager가 있다고 가정하고 경로에 간단한 보안 필터를 추가할 수 있습니다. 다음 예에서는 그렇게 하는 방법을 보여줍니다.
SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST("/person", handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();
앞의 예는 next.handle(ServerRequest) 호출이 선택 사항임을 보여줍니다. 접근이 허용될 때만 핸들러 함수가 실행되도록 합니다.
라우터 기능 빌더에서 filter 메서드를 사용하는 것 외에도 RouterFunction.filter(HandlerFilterFunction)를 통해 기존 라우터 기능에 필터를 적용할 수 있습니다.
기능적 끝점에 대한 CORS 지원은 전용 CorsFilter를 통해 제공됩니다.