이전에 서블릿(Servlet)를 기반으로 개발한 디스패처 서블릿(DispatcherServlet)의 컨트롤러(Controller)에 어노테이션(Annotation)을 이용하여 "Request Mapping"를 처리하도록 하겠습니다.
1. 클래스가 컨트롤러(Controller)로 인식되기 위해 어노테이션(Annotation)으로 생성합니다.
2. 요청(Request) URL과 클래스의 메소드가 매핑되도록 어노테이션(Annotation)를 생성합니다.
3. 클래스에 어노테이션(Annotation)을 추가합니다.
4. WAS 로딩 시점에 프로젝트 패키지에 있는 클래스들을 스캔하여 클래스의 인스턴스를 생성하고 클래스의 메소드(Method)를 추출하여 요청매핑맵(Request Mapping Map)를 생성합니다.
5. 디스패처 서블릿(DispatcherServlet)에서 요청매핑맵(Request Mapping Map)를 이용하여 처리합니다.
클래스가 컨트롤러(Controller)로 인식되기 위해 어노테이션(Annotation)으로 생성합니다.
1. "test2" 프로젝트의 "Java Resources"에서 "src/main/java"의 "com.hom.project.test2.annotation"에서 오른쪽 버튼을 클릭하여 컨텍스트 메뉴 [New > Annotation]를 클릭합니다.
"New Annotation Type"창에서 "Name"에 "TestAnnController"를 입력하고 "@Retention"은 "Runtime", "@Target"은 "Type"를 선택하고 "Finish"버튼을 클릭합니다.
"TestController"로 어노테이션을 생성할려고 했으나 기존에 동일한 클래스(TestController.java)가 있어서 "TestAnnController"로 하였습니다. 반드시 클래스명은 동일하지 않아야 합니다.
"@TestAnnController"어노테이션(Annotation)은 클래스가 컨트롤러(Controller)로 인식하기 위한 것으로 추가 정보인 엘리먼트(element)가 필요하지 않습니다.
요청(Request) URL과 클래스의 메소드가 매핑되도록 어노테이션(Annotation)를 생성합니다.
2. "test2" 프로젝트의 "Java Resources"에서 "src/main/java"의 "com.hom.project.test2.annotation"에서 오른쪽 버튼을 클릭하여 컨텍스트 메뉴 [New > Annotation]를 클릭합니다.
"New Annotation Type"창에서 "Name"에 "TestAnnRequestMapping"를 입력하고 "@Retention"은 "Runtime", "@Target"은 "Type"과 "Method"를 선택하고 "Finish"버튼을 클릭합니다.
"@TestAnnRequestMapping"어노테이션(Annotation)은 클래스의 메소드(Method)가 요청(Request) URL과 매핑되도록 추가 정보인 엘리먼트(element)를 추가합니다.
요청(Request) URL를 값으로 받기위해 문자열인 "value"를 추가합니다. 기본값으로 ""(공백)를 설정합니다.
public String value() default "";
요청(Request) 전송방식(Method)를 값으로 받기위해 enum타입의 "method"를 추가합니다. 기본값으로 "GET"(GET 방식)으로 설정합니다.
public enum RequestMethod {GET, POST}
public RequestMethod method() default RequestMethod.GET;
클래스에 어노테이션(Annotation)을 추가합니다.
3. "TestRootController.java"의 클래스 위에 "@TestAnnController"어노테이션(Annotation)를 추가합니다.
@TestAnnController
public class TestRootController extends TestController {
"TestRootController.java"의 "test1"메소드 위에 "@TestAnnRequestMapping"어노테이션(Annotation)를 추가하고 "value"에 요청(Request) URL로 "/test1.do"를 입력하고 "method"에 "GET"(GET 방식)를 설정합니다.
@TestAnnRequestMapping(value = "/test1.do", method = RequestMethod.GET)
public ModelAndView test1(HttpServletRequest request, HttpServletResponse response) {
"test2"메소드도 같은 방식으로 추가합니다.
@TestAnnRequestMapping(value = "/test2.do", method = RequestMethod.GET)
public ModelAndView test2(HttpServletRequest request, HttpServletResponse response) {
WAS 로딩 시점에 프로젝트 패키지에 있는 클래스들을 스캔하여 클래스의 인스턴스를 생성하고 클래스의 메소드(Method)를 추출하여 요청매핑맵(Request Mapping Map)를 생성합니다.
기존의 "TestController"의 기능인 "addRequestFunction"메소드를 이용하여 처리되게 하겠습니다.
4. "TestServletContextListener.java"에 요청매핑맵(Request Mapping Map)을 추가합니다.
Map<String, ITestController> requestMappingMap = new HashMap<String, ITestController>();
"TestServletContextListener.java"의 "contextInitialized"에서 클래스들을 스캔한 클래스 배열에서 "@TestAnnController"어노테이션(Annotation)을 가지고 있는 클래스를 추출하도록 수정합니다.
List<Class<?>> classes = TestClassScanner.scan(basePackage);
for (Class<?> findClass : classes) {
//System.out.println("[Class] " + findClass.getName());
}
스캔한 클래스에서 "@TestAnnController"어노테이션(Annotation)이 있는지 확인하고 클래스의 인스턴스를 생성합니다.
TestAnnController annoController = findClass.getAnnotation(TestAnnController.class);
if (null != annoController) {
Object controllerInstance = null;
try {
controllerInstance = findClass.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
if (null != controllerInstance) {
ITestController controller = (ITestController) controllerInstance;
}
}
스캔한 클래스에서 "@TestAnnRequestMapping"어노테이션(Annotation)이 있는지 확인하고 인스턴스를 생성 클래스의 addRequestFunction"메소드를 호출하려 요청(Request) URL과 클래스의 메소드를 매핑하고 요청매핑맵(Request Mapping Map)에 추가합니다.
Method[] methods = findClass.getDeclaredMethods();
for (Method method : methods) {
TestAnnRequestMapping annoRequestMapping = method.getAnnotation(TestAnnRequestMapping.class);
if (null != annoRequestMapping) {
if (controller.addRequestFunction(annoRequestMapping.value(), method.getName())) {
requestMappingMap.put(annoRequestMapping.value(), controller);
}
}
}
서블릿 콘텍스트(ServletContext)에 추가합니다.
sce.getServletContext().setAttribute("RequestMapping", requestMappingMap);
"contextInitialized"메소드 전체 소스입니다.
/**
* @see ServletContextListener#contextInitialized(ServletContextEvent)
*/
public void contextInitialized(ServletContextEvent sce) {
System.out.println("ServletContext 초기화 실행");
String basePackage = sce.getServletContext().getInitParameter("ComponentScan-BasePackage");
System.out.println("basePackage : " + basePackage);
Map<String, ITestController> requestMappingMap = new HashMap<String, ITestController>();
System.out.println("[RequestMapping]를 시작합니다.");
List<Class<?>> classes = TestClassScanner.scan(basePackage);
for (Class<?> findClass : classes) {
//System.out.println("[Class] " + findClass.getName());
TestAnnController annoController = findClass.getAnnotation(TestAnnController.class);
if (null != annoController) {
Object controllerInstance = null;
try {
controllerInstance = findClass.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
//e.printStackTrace();
System.out.println(findClass.getName() + "의 컨트롤러 인스턴스를 생성하지 못함");
}
if (null != controllerInstance) {
ITestController controller = (ITestController) controllerInstance;
Method[] methods = findClass.getDeclaredMethods();
for (Method method : methods) {
TestAnnRequestMapping annoRequestMapping = method.getAnnotation(TestAnnRequestMapping.class);
if (null != annoRequestMapping) {
if (controller.addRequestFunction(annoRequestMapping.value(), method.getName())) {
requestMappingMap.put(annoRequestMapping.value(), controller);
}
}
}
}
}
}
System.out.println("[RequestMapping]를 종료합니다.");
sce.getServletContext().setAttribute("RequestMapping", requestMappingMap);
}
디스패처 서블릿(DispatcherServlet)에서 요청매핑맵(Request Mapping Map)를 이용하여 처리합니다.
5. "TestDispatcherServlet.java"의 맴버 필드인 맵들을 주석처리합니다.
//private Map<String, Object> classMappingMap = new HashMap<String, Object>();
//private Map<String, Object> requestMappingMap = new HashMap<String, Object>();
"TestDispatcherServlet.java"의 "init"메소드에서 프로퍼티(properties)파일로 읽어서 처리한 부분을 주석처리합니다.
@Override
public void init(ServletConfig config) throws ServletException {
System.out.println("TestDispatcherServlet init 실행");
/*
String requestMapping = config.getInitParameter("RequestMapping");
Properties serviceHandlerMapping = new Properties();
InputStream inputStream = getClass().getResourceAsStream(requestMapping);
if (null != inputStream) {
try {
serviceHandlerMapping.load(inputStream);
System.out.println(requestMapping + "을 프로퍼티로 읽음");
} catch (IOException e) {
System.out.println(requestMapping + "을 프로퍼티로 읽지 못함");
e.printStackTrace();
}
} else {
System.out.println(requestMapping + "을 읽지 못함");
}
Iterator<Object> keyIter = serviceHandlerMapping.keySet().iterator();
while (keyIter.hasNext()) {
String url = (String) keyIter.next();
String controllerClassName = serviceHandlerMapping.getProperty(url);
String controllerInfo[] = controllerClassName.split("@");
try
{
ITestController controller = null;
if (!classMappingMap.containsKey(controllerInfo[0])) {
Class<?> controllerClass = Class.forName(controllerInfo[0]);
Object controllerInstance = controllerClass.newInstance();
controller = (ITestController) controllerInstance;
classMappingMap.put(controllerInfo[0], controller);
} else {
controller = (ITestController) classMappingMap.get(controllerInfo[0]);
}
if (controller.addRequestFunction(url, controllerInfo[1])) {
requestMappingMap.put(url, controller);
}
} catch (Exception e) {
System.out.println("[RequestMapping] URL:" + url + "에 " + controllerInfo[0] + "를 생성하지 못하여 매핑하지 못함");
// InstantiationException, IllegalAccessException
throw new ServletException(e);
}
//String serviceClassName = serviceHandlerMapping.getProperty(url);
//try
//{
// Class<?> serviceClass = Class.forName(serviceClassName);
// Object serviceInstance = serviceClass.newInstance();
// ITestService service = (ITestService) serviceInstance;
// requestMappingMap.put(url, service);
// System.out.println("[RequestMapping] URL:" + url + "에 " + serviceClassName + "를 생성하여 매핑함");
//}
//catch (Exception e)
//{
// System.out.println("[RequestMapping] URL:" + url + "에 " + serviceClassName + "를 생성하지 못하여 매핑하지 못함");
// // InstantiationException, IllegalAccessException
// throw new ServletException(e);
//}
}
*/
super.init(config);
}
"TestDispatcherServlet.java"의 "service"메소드에서 서블릿 콘텍스트(ServletContext)에 추가된 "RequestMapping"를 가져옵니다.
@SuppressWarnings("unchecked")
Map<String, ITestController> requestMappingMap = (Map<String, ITestController>)request.getServletContext().getAttribute("RequestMapping");
@SuppressWarnings("unchecked")를 추가한 이유는 "Type safety: Unchecked cast from Object to Map<String, ITestController>"로 객체를 확인되지 않은 유형(Map<String, ITestController>)으로 형 변환에 한 것에 대한 안정성 경고를 무시하기 위해서 입니다.
형 변환에서 "Object"로 저장했다가 가져올 경우 항상 유형의 안정성 보장할 수 없기 때문에 나타나는 경고입니다.
6. "Servers"탭에서 "tomcat8"를 선택하고 "start"버튼(start the server)을 클릭합니다.
"Console"탭을 클릭하면 클래스를 스캔하고 발견된 어노테이션(Annotation)으로 요청(Request) URL과 메소드를 매핑한 것을 확인할 수 있습니다. 이전에 프로퍼티(properties)파일로 읽어서 처리한 것도 동일합니다.
[Console]
ServletContextListener 생성자 실행
HttpSessionListener 생성자 실행
ServletRequestListener 생성자 실행
ServletContextAttributeListener 생성자 실행
ServletContext 초기화 실행
basePackage : com.home.project.test2
[RequestMapping]를 시작합니다.
com.home.project.test2.controller.TestRootController@b0f4477의 test1메소드를 찾음
[RequestMapping] URL : /test1.do에 com.home.project.test2.controller.TestRootController@b0f4477의 test1메소드를 매핑함
com.home.project.test2.controller.TestRootController@b0f4477의 test2메소드를 찾음
[RequestMapping] URL : /test2.do에 com.home.project.test2.controller.TestRootController@b0f4477의 test2메소드를 매핑함
[RequestMapping]를 종료합니다.
servletContext에 속성 추가 - 속성명 : RequestMapping, 속성 값 : {/test1.do=com.home.project.test2.controller.TestRootController@b0f4477, /test2.do=com.home.project.test2.controller.TestRootController@b0f4477}
웹 브라우저에서 "http://localhost:8080/test2/test1.do"를 입력합니다.
"Console"탭을 클릭하면 요청(Request) URL로 메소드가 처리된 것을 확인할 수 있습니다.
[Console]
[RequestMapping] URL : /test1.do요청을 com.home.project.test2.controller.TestRootController@b0f4477로 실행함
이처럼 어노테이션(Annotation)으로 요청(Request) URL과 메소드를 매핑하여 처리할 수 있습니다. "springFramework"의 디스패처 서블릿(DispatcherServlet)과 컨트롤러(Controller)의 동작 방식을 이해하는 도움이 될겁니다.