Java 리플렉션(reflection)으로 만들어보는 dispatcher-servlet,컨트롤러 및 커스텀 어노테이션

2021. 10. 15. 21:42개인노트

반응형

스프링에서 엄청나게 편하게 사용중인 RequestMapping어노테이션의 작동원리를 알아보자!

서블릿과 필터로 만들어보는 dispatcher-servlet을 대신할 필터하나

컨트롤러 부분의 MainController

커스텀 어노테이션 부분의 MyRequestMapping

각각 페이지를 연결시킬 index.jsp join.jsp login.jsp 를 만들어서 준비한다.

 

최초에는 Web 프로젝트를 생성해준다.spring이 아니라 구 방식의 프로젝트

 

Dynamic Web Project 생성모습

그리고 reflect란 이름으로 프로젝트 생성 및 톰캣 9.0사용 및 경로지정.

프로젝트 구성모습

Web.xml에 DisPatcher 서블릿을 대신하여 url를 맵핑하여 연결해줄때 사용할 필터를 등록한다..!

web.xml에 필터 생성 및 등록.

  <filter>
    <filter-name>dispatcher</filter-name>
    <filter-class>com.test.reflect.filter.Dispatcher</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>dispatcher</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

 

그리고 각각의 클래스나 어노테이션을 생성해주었다.

MyRequestMapping 어노테이션 (연결할 url주소를 받기위해 String value를 하나 추가되어있음)

package com.test.reflect.anno;

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;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyRequestMapping {

	String value();
}

MainController (뷰리졸버가 없다는 가정하에 /WEB-INF와 .jsp를 그대로 매칭시켰다.)

아래는 /login /index /join 각각 3개의 연결에 대한 페이지처리를 수행한다.

package com.test.reflect.controller;

import com.test.reflect.anno.MyRequestMapping;

public class MainController {
	
	@MyRequestMapping(value = "/login")
	public String login() {
		System.out.println("login() 호출");
		return "/WEB-INF/login.jsp";
	}
	@MyRequestMapping("/index")
	public String index() {
		System.out.println("index() 호출");
		return "/WEB-INF/index.jsp";
	}
	@MyRequestMapping("/join")
	public String join() {
			System.out.println("join() 호출");
			return "/WEB-INF/join.jsp";
	}

}

 

Dispatcher 필터!!

요청받은 url에 따라서 endPoint를 가져와서 어노테이션에 동일한 value값이 존재하는경우 메서드를 수행하면서 매칭되는 파일에다가 연결시켜준다!

ex)http://localhost:8080/reflect/login를 호출한경우

endPoint = /login

MainController에 MyRquestMapping 어노테이션이 존재하는지 확인한 뒤 존재한다면 value값을 확인하고 

/login과 동일할 경우 메서드를 수행하여 결과값을 받는다(결과값 = /WEB-INF/login.jsp)

그리고 foward방식으로 필터에서 연결시켜준다!

package com.test.reflect.filter;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.test.reflect.anno.MyRequestMapping;
import com.test.reflect.controller.MainController;

public class Dispatcher implements Filter{

	@Override
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		
		//URL주소 매핑을 위해 파싱하기(replace를 수행하면 기존 contextpath인 /reflect 를 제외한걸 가져옴.
		String endPoint  = request.getRequestURI().replaceAll(request.getContextPath(), "");
		System.out.println(endPoint);//실제 호출한 주소 ex) /login
		MainController mainController = new MainController();
		
		Method[] methods =  mainController.getClass().getDeclaredMethods();//해당 클래스에 존재하는 모든 메서드를 가져온다
		for(Method method : methods) {
			//어노테이션이 한개인경우
			/*
			 Annotation annotation =method.getDeclaredAnnotation(MyRequestMapping.class);
			 */
			//어노테이션이 여러개인경우
			Annotation[] annotations =method.getDeclaredAnnotations();
			Annotation annotation=null;
			for(Annotation anno : annotations) {
				if(anno.annotationType().equals(MyRequestMapping.class)) {
					annotation = anno;
				}
				break;
			}
			MyRequestMapping myRequestMapping = (MyRequestMapping) annotation;
			String value = myRequestMapping.value(); //어노테이션에 선언된 value값(URL주소 매핑을 위한 값을 받는부분)
			if(value.equals(endPoint)) {
				try {
					String path = (String) method.invoke(mainController);
					RequestDispatcher dis = request.getRequestDispatcher(path);
					dis.forward(request, response);
				} catch (Exception e) {
					e.printStackTrace();
				}
				break;
			}
			
		}
		
	}

}

 

각각의 페이지를 요청하면 해당페이지가 나타난다. 

로그인 URL요청시
Join요청시
Index 요청시

 

##그리고 !! Controller에 DTO나 VO가 존재하여 거기에 해당되는 값을 받을때 자동으로 주입받는 형태가 스프링에서는 지원이 되는데..!##

테스트용 LoginDTO JoinDTO 생성

package com.test.reflect.dto;

public class LoginDTO {
	private String username;
	private String password;
	@Override
	public String toString() {
		return "LoginDTO [username=" + username + ", password=" + password + "]";
	}
	public String getUsername() {
		return username;
	}
	public void setUsername(String username) {
		this.username = username;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
	
	
}

 

package com.test.reflect.dto;

public class JoinDTO {
	private String username;
	private String password;
	private String email;
	
	@Override
	public String toString() {
		return "JoinDTO [username=" + username + ", password=" + password + ", email=" + email + "]";
	}
	public String getUsername() {
		return username;
	}
	public void setUsername(String username) {
		this.username = username;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
	public String getEmail() {
		return email;
	}
	public void setEmail(String email) {
		this.email = email;
	}
}

 

그리고 MainController를 각각의 DTO를 사용하도록 변경!

package com.test.reflect.controller;

import com.test.reflect.anno.MyRequestMapping;
import com.test.reflect.dto.JoinDTO;
import com.test.reflect.dto.LoginDTO;

public class MainController {
	
	@MyRequestMapping(value = "/login")
	public String login(LoginDTO logindto) {
		System.out.println("login() 호출");
		System.out.println(logindto);
		return "/WEB-INF/login.jsp";
	}
	@MyRequestMapping("/index")
	public String index() {
		System.out.println("index() 호출");
		return "/WEB-INF/index.jsp";
	}
	@MyRequestMapping("/join")
	public String join(JoinDTO joindto) {
			System.out.println("join() 호출");
			System.out.println(joindto);
			return "/WEB-INF/join.jsp";
	}

}

Dispatcher필터를 수정 파라미터를 기준으로 setter를 가져올 수 있도록 하는 메서드, 그에 해당하는 이름으로 변경시켜주는 메서드등이 추가 되었다!

package com.test.reflect.filter;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Enumeration;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.test.reflect.anno.MyRequestMapping;
import com.test.reflect.controller.MainController;

public class Dispatcher implements Filter{

	@Override
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		
		//URL주소 매핑을 위해 파싱하기(replace를 수행하면 기존 contextpath인 /reflect 를 제외한걸 가져옴.
		String endPoint  = request.getRequestURI().replaceAll(request.getContextPath(), "");
		System.out.println(endPoint);//실제 호출한 주소 ex) /login
		MainController mainController = new MainController();
		
		Method[] methods =  mainController.getClass().getDeclaredMethods();//해당 클래스에 존재하는 모든 메서드를 가져온다
		for(Method method : methods) {
			//어노테이션이 한개인경우
			/*
			 Annotation annotation =method.getDeclaredAnnotation(MyRequestMapping.class);
			 */
			//어노테이션이 여러개인경우
			Annotation[] annotations =method.getDeclaredAnnotations();
			Annotation annotation=null;
			for(Annotation anno : annotations) {
				if(anno.annotationType().equals(MyRequestMapping.class)) {
					annotation = anno;
				}
				break;
			}
			MyRequestMapping myRequestMapping = (MyRequestMapping) annotation;
			String value = myRequestMapping.value(); //어노테이션에 선언된 value값(URL주소 매핑을 위한 값을 받는부분)
			if(value.equals(endPoint)) {
				try {
					Parameter[] params  = method.getParameters();
					String path = null;
					if(params.length!=0) {//파라미터 있을때
						Object dtoInstance=null;
						for (Parameter param : params) {
							dtoInstance = param.getType().newInstance();//해당 클래스로 만들어준다
							setData(dtoInstance, request);
						}
						path = (String) method.invoke(mainController,dtoInstance);
					}else {//없을때
						path = (String) method.invoke(mainController);
					}
					RequestDispatcher dis = request.getRequestDispatcher(path);
					dis.forward(request, response);
				} catch (Exception e) {
					e.printStackTrace();
				}
				break;
			}
			
		}
		
	}
	private <T> void setData(T instance, HttpServletRequest request) {
		//모든 파라미터의 이름들을 가져온다. ex) username,password
		Enumeration<String> keys = request.getParameterNames();
		//key--> set으로 변형 ex) username --> setUsername
		while(keys.hasMoreElements()) {
			String key = keys.nextElement();
			String methodKey = keyToMethodKey(key);
			Method[] methods = instance.getClass().getDeclaredMethods();
			for (Method method : methods) {
				//만약 username의 파라미터가 존재한다면 setUsername메서드를 찾아서 해당값을 넘겨주고 메서드를 실행시킨다!
				if(method.getName().equals(methodKey)) {
					try {
						method.invoke(instance, request.getParameter(key));
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}//methods
		}//while
	}
	/**
	 * 파라미터 값을 받으면 setter의 기본형태로 만들어서 string을 return한다
	 * @param key
	 * @return String methodKey
	 */
	private String keyToMethodKey(String key) {
		String firstKey = "set";
		String upperKey = key.substring(0,1).toUpperCase();
		String remainKey = key.substring(1);
		return firstKey+upperKey+remainKey;
	}

}

 

##일반적인 요청시 파라미터가 없는경우##

login호출시 DTO에 셋팅된 값 모습

##파라미터가 존재하고 postman을통해 요청했을때##

Postman 요청화면
DTO에 리플렉션을 이용하여 Setter를 찾아서 넣어준화면

DTO에 존재하는 setter를 찾아서 제대로 작동한 모습이다!!

자바 리플렉션을 통하여 스프링에서 어노테이션등을 붙여서 자동으로 페이지 연결 시켜주거나, 그 안에있는 DTO나 VO등에 set을 해주는 것 등을 어떠한 방식으로 하는지 알아보았다!

반응형