이번에 정리 할 내용은 HTTP 요청을 handler method 에 매핑하는 방법이다.
MyHelloController 를 제외한 Person, PersonFormatter, GreetingInterceptor, AnotherInterceptor 를 삭제하고 WebConfig 의 Interceptor, ResourceHandler, Jaxb 설정도 삭제했다.
MyHelloControllerTest 클래스의 내용도 모두 삭제 했다.
MyHelloController 에 다음과 같이 handler method 를 하나 등록 하였다.
package me.nimkoes;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MyHelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "hello";
}
}
@Controller annotation 을 사용해서 component scan 대상으로 만들고, 그 안에 @RequestMapping annotation 을 사용해서 uri 를 지정해주면 handler method 가 된다.
만약 11라인의 @ResponseBody 를 사용하지 않으면 return 하는 문자열에 해당하는 view 를 찾고, 사용 할 경우 반환하는 값을 HTTP response body 에 실어 응답 데이터로 보내게 된다.
추가로 @RequestMapping 을 사용해서 uri 만 지정할 경우 모든 종류의 HTTP method 를 허용하게 된다.
HTTP method 의 종류로는 GET, POST, HEAD, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH 등이 있다.
실제로 그런지 테스트 코드를 작성해서 확인해 봤다.
package me.nimkoes;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void helloTest() throws Exception {
mockMvc.perform(get("/hello")).andDo(print()).andExpect(status().isOk());
mockMvc.perform(post("/hello")).andDo(print()).andExpect(status().isOk());
mockMvc.perform(head("/hello")).andDo(print()).andExpect(status().isOk());
mockMvc.perform(put("/hello")).andDo(print()).andExpect(status().isOk());
mockMvc.perform(delete("/hello")).andDo(print()).andExpect(status().isOk());
mockMvc.perform(options("/hello")).andDo(print()).andExpect(status().isOk());
mockMvc.perform(patch("/hello")).andDo(print()).andExpect(status().isOk());
}
}
만약 특정한 요청에 대해서만 허용하게 하려면 @RequestMapping annotation 에 값을 추가하면 된다.
package me.nimkoes;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MyHelloController {
@RequestMapping(value = "/hello", method = RequestMethod.GET)
@ResponseBody
public String hello() {
return "hello";
}
}
RequestMethod 는 enum 타입으로 HTTP request method 를 정의하고 있다.
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.bind.annotation;
/**
* Java 5 enumeration of HTTP request methods. Intended for use with the
* {@link RequestMapping#method()} attribute of the {@link RequestMapping} annotation.
*
* <p>Note that, by default, {@link org.springframework.web.servlet.DispatcherServlet}
* supports GET, HEAD, POST, PUT, PATCH and DELETE only. DispatcherServlet will
* process TRACE and OPTIONS with the default HttpServlet behavior unless explicitly
* told to dispatch those request types as well: Check out the "dispatchOptionsRequest"
* and "dispatchTraceRequest" properties, switching them to "true" if necessary.
*
* @author Juergen Hoeller
* @since 2.5
* @see RequestMapping
* @see org.springframework.web.servlet.DispatcherServlet#setDispatchOptionsRequest
* @see org.springframework.web.servlet.DispatcherServlet#setDispatchTraceRequest
*/
public enum RequestMethod {
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
}
이제 이 handler 는 HTTP GET 요청만 처리할 수 있다.
테스트 코드를 다음과 같이 고쳐 실행해 보았다.
package me.nimkoes;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void helloTest() throws Exception {
mockMvc.perform(get("/hello"))
.andDo(print())
.andExpect(status().isOk())
;
mockMvc.perform(post("/hello"))
.andDo(print())
.andExpect(status().isMethodNotAllowed())
;
}
}
handler 가 GET 방식의 요청만 처리할 수 있게 되었으므로 POST 요청에 대한 status 로 405 Method Not Allowed 를 반환하게 된다.
만약 HTTP GET 과 POST 를 모두 허용하고 싶으면 handler 를 다음과 같이 method 를 배열 형태로 수정하면 된다.
package me.nimkoes;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MyHelloController {
@RequestMapping(value = "/hello", method = {RequestMethod.GET, RequestMethod.POST})
@ResponseBody
public String hello() {
return "hello";
}
}
지금은 @RequestMapping annotation 을 사용해서 HTTP 의 어떤 타입의 요청을 처리할지 mehotd 값을 지정해 주었지만 한가지 타입의 요청만 처리하도록 한다면 @GetMapping 과 같은 다른 annotation 을 사용해서 코드를 간결하게 작성할 수 있다.
package me.nimkoes;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MyHelloController {
@GetMapping("/hello")
@ResponseBody
public String hello() {
return "hello";
}
}
@GetMapping 을 포함하여 @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping 등이 있다.
그리고 이 모든 HTTP request method 에 매칭하는 annotation 은 @RequestMapping annotation 을 meta annotation 으로 사용하고 있다.
@GetMapping 을 대표로 문서를 보면 @RequestMapping(method = RequestMethod.GET) 의 축약 표현이라고 정의되어 있다.
추가로 @RequstMapping 은 method 가 아닌 class 레벨에 작성할 수도 있다.
package me.nimkoes;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping(method = RequestMethod.GET)
public class MyHelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "hello";
}
@PostMapping
@ResponseBody
public String hi() {
return "hi";
}
}
class 레벨에 @ReauestMapping 으로 GET 요청만 받을 수 있도록 정의 했기 때문에 이 안에 작성된 모든 handler 는 GET 요청만 처리할 수 있게 된다.
실제로 그런지 궁금해서 일부러 @PostMapping 을 추가하고 테스트 코드를 작성해봤다.
package me.nimkoes;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void helloTest() throws Exception {
mockMvc.perform(get("/hello")).andExpect(status().isOk()); // 200
mockMvc.perform(post("/hello")).andExpect(status().isMethodNotAllowed()); // 405
mockMvc.perform(get("/hi")).andExpect(status().isNotFound()); // 404
mockMvc.perform(post("/hi")).andExpect(status().isNotFound()); // 404
}
}
특이했던건 @PostMapping 을 정의한 handler 에 대해서 405 가 아닌 404 응답을 받았다는 점이다.
HTTP request 를 handler 에 매핑할 때 uri 가 완전히 동일한 handler 만 사용했는데 패턴을 사용하는 방법이 있다.
패턴은 크게 다음 세 가지를 사용할 수 있다.
? | 길이 1의 문자 필수 | /hello? /hello?? /hello/? /hello/?? /hello?/?/? /hello/?test? |
* | 길이 0 이상의 문자열 | /hello* /hello/* /hello/*/test /hello/*/te*st |
** | 하위 모든 path | /hello/** /hello/**/test |
사용하기에 따라 굉장히 복잡한 규칙을 만들 수 있겠지만 그런 경우가 얼마나 있을까 싶은 생각이다.
package me.nimkoes;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyHelloController {
@GetMapping("/hello?")
public String hello_case_1() { return "hello case 1"; }
@GetMapping("/hello/?")
public String hello_case_2() { return "hello case 2"; }
@GetMapping("/hello/??")
public String hello_case_3() { return "hello case 3"; }
@GetMapping("/hello/?/?")
public String hello_case_4() { return "hello case 4"; }
@GetMapping("/hello*")
public String hello_case_5() { return "hello case 5"; }
@GetMapping("/hello/*/test")
public String hello_case_6() { return "hello case 6"; }
@GetMapping("/hello/*/te*st")
public String hello_case_7() { return "hello case 7"; }
@GetMapping("/hello/**")
public String hello_case_8() { return "hello case 8"; }
@GetMapping("/hello/**/test")
public String hello_case_9() { return "hello case 9"; }
}
어떻게 사용할 수 있는지 테스트 해보기 위해 몇가지 경우만 만들어 보았다.
package me.nimkoes;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void helloTest() throws Exception {
mockMvc.perform(get("/hello1")) .andExpect(content().string("hello case 1"));
mockMvc.perform(get("/hello!")) .andExpect(content().string("hello case 1"));
mockMvc.perform(get("/hello/z")) .andExpect(content().string("hello case 2"));
mockMvc.perform(get("/hello/nk")) .andExpect(content().string("hello case 3"));
mockMvc.perform(get("/hello/n/k")) .andExpect(content().string("hello case 4"));
mockMvc.perform(get("/hello")) .andExpect(content().string("hello case 5"));
mockMvc.perform(get("/helloTest")) .andExpect(content().string("hello case 5"));
mockMvc.perform(get("/hello/nimkoes/test")) .andExpect(content().string("hello case 6"));
mockMvc.perform(get("/hello/nk/te_some_st")) .andExpect(content().string("hello case 7"));
mockMvc.perform(get("/hello/nk/some")) .andExpect(content().string("hello case 8"));
mockMvc.perform(get("/hello/nk/some/another")) .andExpect(content().string("hello case 8"));
mockMvc.perform(get("/hello/nk/some/another/test")).andExpect(content().string("hello case 9"));
}
}
uri 패턴 매핑 방법 중에 확장자를 매핑하는 방법도 있다.
이 방법은 요청 uri 가 특정 파일의 확장자를 포함하는 경우인데 /hello.* 와 같은 uri 패턴을 사용할 경우 /hello.json, /hello.zip 등의 uri 를 사용할 수 있는 것을 뜻한다.
하지만 확장자를 사용한 uri 패턴을 사용할 경우 RFD 공격과 같은 보안 이슈가 있기 때문에 권장하는 방법이 아니며, Spring Boot 에서는 이런 uri 의 요청을 사용하지 못하도록 막고 있다.
@RequestMapping 또는 @RequestMapping 을 meta annotation 으로 사용하는 @GetMapping 과 같은 annotation 을 조금 더 자세히 보면 consumes 와 produces 라는 값을 설정할 수 있다.
// 생략...
package org.springframework.web.bind.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
// 생략...
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
// 생략...
/**
* The consumable media types of the mapped request, narrowing the primary mapping.
* <p>The format is a single media type or a sequence of media types,
* with a request only mapped if the {@code Content-Type} matches one of these media types.
* Examples:
* <pre class="code">
* consumes = "text/plain"
* consumes = {"text/plain", "application/*"}
* </pre>
* Expressions can be negated by using the "!" operator, as in "!text/plain", which matches
* all requests with a {@code Content-Type} other than "text/plain".
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings override
* this consumes restriction.
* @see org.springframework.http.MediaType
* @see javax.servlet.http.HttpServletRequest#getContentType()
*/
String[] consumes() default {};
/**
* The producible media types of the mapped request, narrowing the primary mapping.
* <p>The format is a single media type or a sequence of media types,
* with a request only mapped if the {@code Accept} matches one of these media types.
* Examples:
* <pre class="code">
* produces = "text/plain"
* produces = {"text/plain", "application/*"}
* produces = MediaType.APPLICATION_JSON_UTF8_VALUE
* </pre>
* <p>It affects the actual content type written, for example to produce a JSON response
* with UTF-8 encoding, {@link org.springframework.http.MediaType#APPLICATION_JSON_UTF8_VALUE} should be used.
* <p>Expressions can be negated by using the "!" operator, as in "!text/plain", which matches
* all requests with a {@code Accept} other than "text/plain".
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings override
* this produces restriction.
* @see org.springframework.http.MediaType
*/
String[] produces() default {};
}
consumes 와 produces 모두 header 의 Mediatype 을 참조해서 handler 를 매핑하는데 사용한다.
차이가 있다면 consumes 는 HTTP 요청의 Content-Tpye 을 사용하고 produces 는 Accept 를 사용 한다.
package me.nimkoes;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MyHelloController {
@GetMapping(value = "/hello", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public String hello() {
return "hello";
}
}
HTTP GET 요청을 받는 /hello handler method 는 consumes 로 APPLICATION_JSON_UTF8_VALUE 를 사용하고 있다.
사실 MediaType 타입의 값 대신 문자열을 사용해도 되지만 오타가 있을 경우 찾아내기 매우 어려울 수 있기 때문에 되도록 정의 된 상수를 사용하는걸 권장한다.
APPLICATION_JSON_UTF8_VALUE 는 "application/json;charset=UTF-8" 값이 할당되어 있다.
그래서 이 handler 는 HTTP GET 방식의 uri 가 /hello 여야 하기도 하지만, Content-Type 이 json 형식이라고 명시되어 있어야 매핑할 수 있다.
package me.nimkoes;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void helloTest() throws Exception {
mockMvc.perform(
get("/hello")
.contentType(MediaType.APPLICATION_JSON_UTF8)
)
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string("hello"))
;
}
}
contentType 을 지정해주지 않거나 json 형식이 아닌 경우 테스트는 실패한다.
지금은 contentType 을 올바르게 설정 했기 때문에 테스트는 성공한다.
MockHttpServletRequest: HTTP Method = GET Request URI = /hello Parameters = {} Headers = {Content-Type=[application/json;charset=UTF-8]} Body = null Session Attrs = {} Handler: Type = me.nimkoes.MyHelloController Method = public java.lang.String me.nimkoes.MyHelloController.hello() Async: Async started = false Async result = null Resolved Exception: Type = null ModelAndView: View name = null View = null Model = null FlashMap: Attributes = null MockHttpServletResponse: Status = 200 Error message = null Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[5]} Content type = text/plain;charset=UTF-8 Body = hello Forwarded URL = null Redirected URL = null Cookies = [] |
HTTP request 를 보낼 때 header 에 내가 보내는 데이터의 형식이 무엇인지 알려주는 Content-Type 뿐만 아니라, 요청에 대한 응답 데이터의 형식이 어떤 형식이기를 바란다는 의미로 Accept 값을 설정할 수 있다.
그리고 handler 에도 HTTP request 의 Accept 에 매칭하는 값으로 produces 값을 설정할 수 있는데, 이 handler 의 응답 데이터 형식이 무엇인지 명시할 수 있다.
package me.nimkoes;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MyHelloController {
@GetMapping(
value = "/hello",
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE,
produces = MediaType.TEXT_PLAIN_VALUE
)
@ResponseBody
public String hello() {
return "hello";
}
}
이 handler 의 응답 데이터 형식을 "text/plain" 이라고 명시해 주었기 때문에 HTTP request 의 Accept 값이 다른 값인 경우 오류가 발생 한다.
package me.nimkoes;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void helloTest() throws Exception {
mockMvc.perform(
get("/hello")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
)
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string("hello"))
;
}
}
28라인에서 차라리 accept 를 설정하지 않았다면 테스트는 오히려 성공 했을 텐데, 이 요청에 대한 응답 결과 json 형식의 데이터를 기대한다고 명시했기 때문에 406 Not Acceptable 오류가 발생 한다.
MockHttpServletRequest: HTTP Method = GET Request URI = /hello Parameters = {} Headers = {Content-Type=[application/json], Accept=[application/json]} Body = <no character encoding set> Session Attrs = {} 생략... MockHttpServletResponse: Status = 406 Error message = null Headers = {} Content type = null Body = Forwarded URL = null Redirected URL = null Cookies = [] 생략... java.lang.AssertionError: Status Expected :200 Actual :406 |
정리하면 @RequestMapping 또는 이 annotation 을 meta annotation 으로 사용하는 @GetMapping, @PostMapping 등을 사용할 때 consumes 와 produces 값을 추가로 설정할 수 있는데,
consumes 는 HTTP request header 의 Content-Type 을 참조하고, produces 는 HTTP request header 의 Accept 값을 참조 한다.
Content-Type 이 일치하지 않는 경우 415 Unsupported Media Type 오류가 발생하고
Accept 값이 일치하지 않는 경우 406 Not Acceptable 오류가 발생 한다.
@RequestMapping 에 설정할 수 있는 값으로 headers 가 있다.
// 생략...
package org.springframework.web.bind.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
// 생략...
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
// 생략...
/**
* The headers of the mapped request, narrowing the primary mapping.
* <p>Same format for any environment: a sequence of "My-Header=myValue" style
* expressions, with a request only mapped if each such header is found
* to have the given value. Expressions can be negated by using the "!=" operator,
* as in "My-Header!=myValue". "My-Header" style expressions are also supported,
* with such headers having to be present in the request (allowed to have
* any value). Finally, "!My-Header" style expressions indicate that the
* specified header is <i>not</i> supposed to be present in the request.
* <p>Also supports media type wildcards (*), for headers such as Accept
* and Content-Type. For instance,
* <pre class="code">
* @RequestMapping(value = "/something", headers = "content-type=text/*")
* </pre>
* will match requests with a Content-Type of "text/html", "text/plain", etc.
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit
* this header restriction (i.e. the type-level restriction
* gets checked before the handler method is even resolved).
* @see org.springframework.http.MediaType
*/
String[] headers() default {};
// 생략...
}
이 값은 HTTP request 에 특정한 header 가 있는 경우 또는 그 header 의 값이 특정한 값인 경우에만 handler 를 매핑해주고 싶을 때 사용할 수 있다.
package me.nimkoes;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MyHelloController {
@GetMapping(value = "/hello", headers = HttpHeaders.AUTHORIZATION)
@ResponseBody
public String hello() {
return "hello";
}
}
이 경우 HTTP request header 에 AUTHORIZATION 이 있는 경우 매핑될 수 있다.
HttpHeaders 는 header 로 사용할 수 있는 문자열을 정의 한 클래스이다.
테스트 코드를 작성할 때 보면 headers 로 key-value 쌍으로 설정할 수 있다.
현재 AUTHORIZATION 이 있을 경우에 handler 에 매핑될 수 있기 때문에 없는 경우와 있는 경우 모두 테스트 해보았다.
headers 를 아무것도 설정하지 않았을 때는 404 오류가 발생했다.
java.lang.AssertionError: Status Expected :200 Actual :404 |
package me.nimkoes;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void helloTest() throws Exception {
mockMvc.perform(
get("/hello")
.header(HttpHeaders.AUTHORIZATION,"1234")
)
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string("hello"))
;
}
}
MockHttpServletRequest: HTTP Method = GET Request URI = /hello Parameters = {} Headers = {Authorization=[1234]} Body = <no character encoding set> Session Attrs = {} Handler: Type = me.nimkoes.MyHelloController Method = public java.lang.String me.nimkoes.MyHelloController.hello() Async: Async started = false Async result = null Resolved Exception: Type = null ModelAndView: View name = null View = null Model = null FlashMap: Attributes = null MockHttpServletResponse: Status = 200 Error message = null Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[5]} Content type = text/plain;charset=UTF-8 Body = hello Forwarded URL = null Redirected URL = null Cookies = [] |
HTTP request header 에 특정한 key 가 있는 경우에만 매핑할 뿐만 아니라 그 값에 대해서도 검사할 수 있다.
예를 들어 지금 AUTHORIZATION 값으로 "1234" 를 설정 했는데, @GetMapping 에서도 이 값이 올바른 경우에만 매핑해줄 수 있다.
package me.nimkoes;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MyHelloController {
@GetMapping(value = "/hello", headers = HttpHeaders.AUTHORIZATION + "=" + "4321")
@ResponseBody
public String hello() {
return "hello";
}
}
handler 에 설정한 headers 를 보면 HttpHeaders.AUTHORIZATION 의 값이 "4321" 인 경우에만 매핑하도록 설정했기 때문에 테스트를 다시 실행해보면 실패한다.
이 경우에도 앞서 AUTHORIZATION 이 없었을 때와 마찬가지로 404 오류를 발생한다.
테스트 코드에서 AUTHORIZATION 값을 "4321" 로 수정한 다음 실행하면 테스트가 성공한다.
package me.nimkoes;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void helloTest() throws Exception {
mockMvc.perform(
get("/hello")
.header(HttpHeaders.AUTHORIZATION,"4321")
)
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string("hello"))
;
}
}
MockHttpServletRequest: HTTP Method = GET Request URI = /hello Parameters = {} Headers = {Authorization=[4321]} Body = <no character encoding set> Session Attrs = {} Handler: Type = me.nimkoes.MyHelloController Method = public java.lang.String me.nimkoes.MyHelloController.hello() Async: Async started = false Async result = null Resolved Exception: Type = null ModelAndView: View name = null View = null Model = null FlashMap: Attributes = null MockHttpServletResponse: Status = 200 Error message = null Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[5]} Content type = text/plain;charset=UTF-8 Body = hello Forwarded URL = null Redirected URL = null Cookies = [] |
headers 설정과 관련하여 특정 header 가 있는 경우와 그 header 의 값이 특정한 값인 경우에 매핑하는 방법에 대해 알아보았다.
반대로 특정 header 가 없는 경우에 매핑하고 싶은 경우 ! 연산자를 사용해서 설정할 수 있다.
package me.nimkoes;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MyHelloController {
@GetMapping(value = "/hello", headers = HttpHeaders.AUTHORIZATION + "=" + "4321")
@ResponseBody
public String hello() {
return "hello";
}
@GetMapping(value = "/hi", headers = "!" + HttpHeaders.AUTHORIZATION)
@ResponseBody
public String hi() {
return "hi";
}
}
새로운 handler 를 정의 하였고, 이 handler 는 HttpHeaders.AUTHORIZATION 이 없는 경우 매핑되도록 설정 했다.
package me.nimkoes;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void helloTest() throws Exception {
mockMvc.perform(
get("/hello")
.header(HttpHeaders.AUTHORIZATION,"4321")
)
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string("hello"))
;
mockMvc.perform(
get("/hi")
.header(HttpHeaders.AUTHORIZATION,"1234")
)
.andDo(print())
.andExpect(status().isNotFound())
;
mockMvc.perform(get("/hi"))
.andDo(print())
.andExpect(status().isOk())
;
}
}
지금까지 header 에 특정한 key 가 있어야 하는 경우, 있을 때 특정한 값이어야 하는 경우, 특정한 key 가 없어야 하는 경우에 대해 정리해 보았다.
추가로 parameter 에 대해서도 header 를 설정한 것과 매우 유사하게 사용할 수 있는데, 단순히 headers 대신 params 를 사용하면 된다.
@GetMapping(value = "/helloParam", params = "test")
@ResponseBody
public String helloParam() {
return "hello";
}
HTTP request 를 이 handler 에 매핑하기 위해서는 "test" 라는 param 이 있어야 한다.
mockMvc.perform(
get("/helloParam")
.param("test", "1234")
)
.andDo(print())
.andExpect(status().isOk())
;
MockHttpServletRequest: HTTP Method = GET Request URI = /helloParam Parameters = {test=[1234]} Headers = {} Body = <no character encoding set> Session Attrs = {} Handler: Type = me.nimkoes.MyHelloController Method = public java.lang.String me.nimkoes.MyHelloController.helloParam() Async: Async started = false Async result = null Resolved Exception: Type = null ModelAndView: View name = null View = null Model = null FlashMap: Attributes = null MockHttpServletResponse: Status = 200 Error message = null Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[5]} Content type = text/plain;charset=UTF-8 Body = hello Forwarded URL = null Redirected URL = null Cookies = [] |
headers 에서와 마찬가지로 params 도 특정 param 이 없어야 하거나, 특정 값이어야 하는 경우 설정 방법이 동일하기 때문에 굳이 반복해서 정리하지 않기로 했다.
Spring Web MVC 를 사용하면 만들지 않아도 HTTP HEAD 와 OPTIONS 요청을 처리할 수 있다.
HTTP HEAD 명세와 HTTP OPTIONS 명세를 한 번 보면 좋을것 같다.
MyHelloController 에 GET 과 POST 요청을 받을 수 있도록 handler 를 수정했다.
package me.nimkoes;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MyHelloController {
@RequestMapping(value = "/hello", method = {RequestMethod.GET, RequestMethod.POST})
@ResponseBody
public String hello() {
return "hello";
}
}
HTTP HEAD 와 OPTIONS 요청을 처리하도록 명시하지 않았지만, Spring Web MVC 를 사용하기 때문에 기본적으로 사용할 수 있다.
정말로 그런지 HEAD 와 OPTIONS 요청에 대한 결과를 테스트 코드를 통해 확인해 봤다.
package me.nimkoes;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.head;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void helloTest() throws Exception {
mockMvc.perform(head("/hello"))
.andDo(print())
.andExpect(status().isOk())
;
mockMvc.perform(options("/hello"))
.andDo(print())
.andExpect(status().isOk())
;
}
}
MockHttpServletRequest: HTTP Method = HEAD Request URI = /hello Parameters = {} Headers = {} Body = <no character encoding set> Session Attrs = {} Handler: Type = me.nimkoes.MyHelloController Method = public java.lang.String me.nimkoes.MyHelloController.hello() Async: Async started = false Async result = null Resolved Exception: Type = null ModelAndView: View name = null View = null Model = null FlashMap: Attributes = null MockHttpServletResponse: Status = 200 Error message = null Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[5]} Content type = text/plain;charset=UTF-8 Body = Forwarded URL = null Redirected URL = null Cookies = [] MockHttpServletRequest: HTTP Method = OPTIONS Request URI = /hello Parameters = {} Headers = {} Body = <no character encoding set> Session Attrs = {} Handler: Type = org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping$HttpOptionsHandler Method = public org.springframework.http.HttpHeaders org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping$HttpOptionsHandler.handle() Async: Async started = false Async result = null Resolved Exception: Type = null ModelAndView: View name = null View = null Model = null FlashMap: Attributes = null MockHttpServletResponse: Status = 200 Error message = null Headers = {Allow=[GET,HEAD,POST,OPTIONS]} Content type = null Body = Forwarded URL = null Redirected URL = null Cookies = [] |
이곳을 통해 각 HTTP method 에 대한 명세를 확인할 수 있다.
마지막으로 커스텀 애노테이션을 만들어서 요청을 매핑하는 방법이 있다.
annotation 중에 다른 annotation 들을 조합해서 만든 annotation 을 composed annotation 이라고 한다.
@GetMapping 역시 composed annotation 이다.
// 생략...
package org.springframework.web.bind.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
// 생략...
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {
// 생략...
}
그리고 @Target, @Retention, @Documented, @RequestMapping 과 같이 다른 annotation 에 정의해서 사용할 수 있는 것을 meta annotation 이라고 한다.
uri 가 /hello 인 HTTP GET 요청을 처리하는 handler 라고 알려줄 수 있는 annotation 을 만들어 봤다.
GetHelloMapping 이라는 이름으로 annotation 을 하나 만들었다.
package me.nimkoes;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public @interface GetHelloMapping {
}
새로 만든 annotation 을 controller 의 handler 에 적용했다.
package me.nimkoes;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyHelloController {
@GetHelloMapping
public String hello() {
return "hello";
}
}
새로 만든 annotation 을 사용해서 handler 매핑이 잘 되는지 확인해보기 위해 테스트 코드를 만들어 실행해 봤다.
package me.nimkoes;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void helloTest() throws Exception {
mockMvc.perform(get("/hello"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string("hello"))
;
}
}
MockHttpServletRequest: HTTP Method = GET Request URI = /hello Parameters = {} Headers = {} Body = <no character encoding set> Session Attrs = {} Handler: Type = me.nimkoes.MyHelloController Method = public java.lang.String me.nimkoes.MyHelloController.hello() Async: Async started = false Async result = null Resolved Exception: Type = null ModelAndView: View name = null View = null Model = null FlashMap: Attributes = null MockHttpServletResponse: Status = 200 Error message = null Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[5]} Content type = text/plain;charset=UTF-8 Body = hello Forwarded URL = null Redirected URL = null Cookies = [] |
Java annotation 에 대해 예전에 정리했던 글을 링크로 남겨둔다.
'Archive > Spring Web MVC' 카테고리의 다른 글
3.3 핸들러 메소드 (execute & using handler method) (작성중) (0) | 2021.07.15 |
---|---|
3.2 핸들러 메소드 : argument & return value (overview, 처리 구현체 자동 등록) (0) | 2021.07.12 |
2.6 HTTP 메시지 컨버터 (JSON, XML) (0) | 2021.07.09 |
2.5 리소스 핸들러 (default Servlet) (1) | 2021.07.08 |
2.4 핸들러 인터셉터 (HandlerInterceptor interface) (0) | 2021.07.07 |