어노테이션을 사용하면 코드의 불필요한 중복을 줄일수 있게 된다. 어노테이션을 만들어보자
일반적으로 uri를 매핑하면 다음의 구조를 가진다
public class UserController {
public void login() {
System.out.println("login() 호출됨");
}
public void join() {
System.out.println("join() 호출됨");
}
}
// 디스패처 서블릿
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String path = sc.nextLine();
UserController uc = new UserController();
if(path.equalsIgnoreCase("/login")) {
uc.login();
}
if(path.equalsIgnoreCase("/join")) {
uc.join();
}
}
사용자가 입력한 주소를 path로 가정했다.
if 조건으로 연결된 주소에 맞는 메소드를 실행시켜서 매핑을 시킨다.
이러한 구조의 단점은 새로운 기능이 필요할 경우 새로운 메소드와 if조건을 추가해야 하는 번거로움이다.
조금 더 편하게 하기 위해서 어노테이션을 만들어 이용해보자
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
String uri();
}
리플렉션이 런타임시에 여러가지를 확인하고 연결해주는데 여기서는 어노테이션을 이용
public class UserController {
@RequestMapping( uri = "/login")
public void login() {
System.out.println("login() 호출됨");
}
@RequestMapping( uri = "/join")
public void join() {
System.out.println("join() 호출됨");
}
}
// 디스패처 서블릿
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String path = sc.nextLine();
UserController uc = new UserController();
Method[] methods = uc.getClass().getDeclaredMethods();
System.out.println(methods.length);
실행하면 @RequestMapping
을 연결시킨 메소드 배열의 길이를 출력해준다.
길이가 출력 된다는것은 컨트롤러의 메소드에 @RequestMapping
을 붙이기만 하면 디스패처 서블릿에서 해당 메소드를 찾을수 있다는 것이다.
그러면 이제부터 일일이 if조건을 만들 필요가 없어진다.
이제 main() 메소드에서 입력받은 uri 만 실행시키고 싶다면 다음의 코드를 이용한다.
Method[] methods = uc.getClass().getDeclaredMethods();
Arrays.asList(methods).forEach((e)->{
Annotation anno = e.getDeclaredAnnotation(RequestMapping.class);
// System.out.println(anno) => @ex01.RequestMapping(uri="/join") 출력됨
RequestMapping rm = (RequestMapping) anno;
// System.out.println(rm.uri()); => /join 출력됨
if(rm.uri().equalsIgnoreCase(path)) {
try {
e.invoke(uc);
} catch (Exception e1) {
e1.printStackTrace();
}
}
});
사용자가 입력한 주소가 존재한다면 해당 주소와 이름이 같은 uri와 연결된 메소드를 실행시켜 준다. (invoke()
로 호출)
이렇게 하면 기능을 보다 쉽게 추가할 수 있게 되었다.
이제 하나의 컨트롤러가 아닌 여러개의 컨트롤러에서 주소를 매핑시켜 보자
지금까지는 @RequestMapping
만 찾아서 매핑했지만 추가적으로 주소가 있는 컨트롤러까지 매핑해야 한다.
컨트롤러를 추가해서 매핑하고 싶다면 새로운 어노테이션을 만들면 된다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}
이렇게 구현하면 스프링에서 사용하는 어노테이션을 만들수가 있다.
@Controller
public class UserController {
@RequestMapping( uri = "/login")
public void login() {
System.out.println("login() 호출됨");
}
@RequestMapping( uri = "/join")
public void join() {
System.out.println("join() 호출됨");
}
@RequestMapping( uri = "/joinForm")
public void joinForm() {
System.out.println("joinForm() 호출됨");
}
@RequestMapping( uri = "/userInfo")
public void userInfo() {
System.out.println("userInfo() 호출됨");
}
}
이번에는 UserController uc = new UserController();
를 만들지 않고 어노테이션이 동작하는지 확인해보자
public static void scan(Class<?> clazz, String path) throws Exception {
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method method : declaredMethods) {
if (method.isAnnotationPresent(RequestMapping.class)) {
Annotation anno = method.getDeclaredAnnotation(RequestMapping.class);
RequestMapping rm = (RequestMapping) anno;
if(rm.uri().equalsIgnoreCase(path)) {
method.invoke(clazz.newInstance());
}
}
}
}
위의 메소드를 사용해서 입력한 uri와 메소드를 매핑시키면
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String path = sc.nextLine();
try {
ControllerScanner.scan(UserController.class, path);
} catch (Exception e) {
e.printStackTrace();
}
}
실행코드가 어노테이션을 찾아서 입력한 uri를 컨트롤러의 메소드로 매핑시켜준다.
이번에는 패키지에서 @Controller
가 붙은 클래스에서 @RequestMapping
을 찾아서 매핑시키는 방법이다.
위에서 만들어 놓은 scan()
를 오버로딩해서 재사용한다.
public static void scan(String packageName, String inputPath) throws Exception {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
String path = packageName.replace(".", "/");
Enumeration<URL> resources = classLoader.getResources(path);
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
File file = new File(resource.toURI());
for (File classFile : file.listFiles()) {
String fileName = classFile.getName();
if (fileName.endsWith(".class")) {
String className = fileName.substring(0, fileName.lastIndexOf("."));
Class<?> clazz = Class.forName(packageName + "." + className);
if (clazz.isAnnotationPresent(Controller.class)) {
scan(clazz, inputPath);
}
}
}
}
}
public static void scan(Class<?> clazz, String path) throws Exception {
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method method : declaredMethods) {
if (method.isAnnotationPresent(RequestMapping.class)) {
Annotation anno = method.getDeclaredAnnotation(RequestMapping.class);
RequestMapping rm = (RequestMapping) anno;
if(rm.uri().equalsIgnoreCase(path)) {
method.invoke(clazz.newInstance());
}
}
}
}
여기서 class loader
의 getContextClassLoader()
를 사용했다.
ClassLoader 클래스
- 자바에서 모든 클래스는 java.lang.ClassLoader 인스턴스에 의해서 로딩된다.
- 한 번 클래스가 JVM으로 로딩되면 같은 클래스는 다시 로딩되지 않는다.
- ClassLoader 인스턴스는 클래스를 로딩하는 것 뿐만 아니라 다른 리소스도 읽을 수 있는 기능을 제공한다.
- 현재 스레드의 getContextClassLoader 메소드를 통해 ClassLoader 인스턴스를 획득할 수 있다.
- ClassLoader 인스턴스를 사용하면 특정 클래스의 인스턴스를 동적으로 생성할 수 있다.
( 레퍼런스 : https://junhyunny.blogspot.com/2019/03/thread-getcontextclassloader.html )
컨트롤러 하나를 추가하고 @Controller
,@RequestMapping
어노테이션을 추가했다.
@Controller
public class BoardController {
@RequestMapping( uri = "/save")
public void save() {
System.out.println("save 호출됨");
}
@RequestMapping( uri = "/super")
public void sse() {
System.out.println("super 호출됨");
}
}
실행 코드에서는 만든 메소드를 호출하고 uri 만 입력한다.
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String path = sc.nextLine();
try {
ControllerScanner.scan("ex01", path);
} catch (Exception e) {
e.printStackTrace();
}
}
패키지 안에 존재하는 컨트롤러를 찾아서 uri 를 매핑시켜주는 모습이다.
약간 다른 코드로는 아래와 같은 코드도 있다.
public class App {
public static Set<Class> componentScan(String pkg) throws Exception {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Set<Class> classes = new HashSet<>();
URL packageUrl = classLoader.getResource(pkg);
File packageDirectory = new File(packageUrl.toURI());
for (File file : packageDirectory.listFiles()) {
if (file.getName().endsWith(".class")) {
String className = pkg + "." + file.getName().replace(".class", "");
//System.out.println(className);
Class cls = Class.forName(className);
classes.add(cls);
}
}
return classes;
}
public static void findUri(Set<Class> classes, String uri) throws Exception {
boolean isFind = false;
for (Class cls : classes) {
if (cls.isAnnotationPresent(Controller.class)) {
Object instance = cls.newInstance();
Method[] methods = cls.getDeclaredMethods();
for (Method mt : methods) {
Annotation anno = mt.getDeclaredAnnotation(RequestMapping.class);
RequestMapping rm = (RequestMapping) anno;
if (rm.uri().equals(uri)) {
isFind = true;
mt.invoke(instance);
}
}
}
}
if(isFind == false){
System.out.println("404 Not Found");
}
}
public static void main(String[] args) throws Exception {
Scanner sc = new Scanner(System.in);
String uri = sc.nextLine();
Set<Class> classes = componentScan("ex02");
findUri(classes, uri);
}
}