diff --git a/build.gradle.kts b/build.gradle.kts index 6b35215f..64599184 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +import org.gradle.internal.impldep.org.junit.Assert.assertThat + plugins { id("java") kotlin("jvm") @@ -24,11 +26,16 @@ dependencies { implementation(group = "org.json", name = "json", version = "20160810") + // aop + implementation("org.reflections:reflections:0.10.2") + // 톰캣 implementation("org.apache.tomcat.embed:tomcat-embed-core:8.5.42") implementation("org.apache.tomcat.embed:tomcat-embed-jasper:8.5.42") implementation(kotlin("stdlib-jdk8")) + + implementation("org.assertj:assertj-core:3.24.2") } tasks.test { diff --git a/src/main/java/com/diy/app/Main.java b/src/main/java/com/diy/app/Main.java index 36237ecf..6832c6ab 100644 --- a/src/main/java/com/diy/app/Main.java +++ b/src/main/java/com/diy/app/Main.java @@ -1,9 +1,12 @@ package com.diy.app; +import com.diy.app.business.repository.LectureRepository; +import com.diy.framework.beans.factory.BeanStorage; import com.diy.framework.web.server.TomcatWebServer; public class Main { public static void main(String[] args) { + BeanStorage beanStorage = BeanStorage.getInstance(); TomcatWebServer tomcatWebServer = new TomcatWebServer(); tomcatWebServer.start(); } diff --git a/src/main/java/com/diy/app/business/controller/LectureController.java b/src/main/java/com/diy/app/business/controller/LectureController.java index 67438c09..494e45a9 100644 --- a/src/main/java/com/diy/app/business/controller/LectureController.java +++ b/src/main/java/com/diy/app/business/controller/LectureController.java @@ -5,22 +5,31 @@ import com.diy.app.infra.dto.ModelAndView; import com.diy.app.infra.httpSpec.HttpMethod; import com.diy.app.infra.port.Controller; +import com.diy.framework.beans.annotations.Autowired; +import com.diy.framework.beans.annotations.Component; import com.fasterxml.jackson.databind.ObjectMapper; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Map; +@Component public class LectureController implements Controller { private LectureService service; - private ObjectMapper objectMapper; + private final ObjectMapper objectMapper; + @Autowired public LectureController(LectureService service) { + objectMapper = new ObjectMapper(); this.service = service; - this.objectMapper = new ObjectMapper(); } + // public LectureController(LectureService service) { +// this.service = service; +// this.objectMapper = new ObjectMapper(); +// } + @Override public ModelAndView handleRequest(HttpServletRequest req, HttpServletResponse resp) throws Exception { String uri = req.getRequestURI(); diff --git a/src/main/java/com/diy/app/business/repository/LectureRepository.java b/src/main/java/com/diy/app/business/repository/LectureRepository.java index 91be4fad..71f86fcd 100644 --- a/src/main/java/com/diy/app/business/repository/LectureRepository.java +++ b/src/main/java/com/diy/app/business/repository/LectureRepository.java @@ -1,6 +1,7 @@ package com.diy.app.business.repository; import com.diy.app.business.domain.Lecture; +import com.diy.framework.beans.annotations.Component; import java.util.List; import java.util.Map; @@ -8,8 +9,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; +@Component public class LectureRepository { - static LectureRepository instance; AtomicLong nextId; Map lectureDB; @@ -19,10 +20,6 @@ private LectureRepository() { lectureDB = new ConcurrentHashMap<>(); } - public static LectureRepository getInstance() { - return instance == null ? instance = new LectureRepository() : instance; - } - public List getAll() { return lectureDB.values().stream().toList(); } diff --git a/src/main/java/com/diy/app/business/service/LectureService.java b/src/main/java/com/diy/app/business/service/LectureService.java index 677fa505..c7281c0e 100644 --- a/src/main/java/com/diy/app/business/service/LectureService.java +++ b/src/main/java/com/diy/app/business/service/LectureService.java @@ -2,22 +2,21 @@ import com.diy.app.business.domain.Lecture; import com.diy.app.business.repository.LectureRepository; +import com.diy.framework.beans.annotations.Autowired; +import com.diy.framework.beans.annotations.Component; import java.util.List; +@Component public class LectureService { - private static LectureService instance = null; - private static LectureRepository repository = null; + private final LectureRepository repository; - private LectureService(LectureRepository repository) { + @Autowired + public LectureService(LectureRepository repository) { this.repository = repository; } - public static LectureService getInstance() { - return instance == null ? instance = new LectureService(LectureRepository.getInstance()) : instance; - } - public List getAllLectures() { return repository.getAll(); } diff --git a/src/main/java/com/diy/app/infra/servlet/UrlControllerMapper.java b/src/main/java/com/diy/app/infra/servlet/UrlControllerMapper.java index 36d6140d..dd7b2b36 100644 --- a/src/main/java/com/diy/app/infra/servlet/UrlControllerMapper.java +++ b/src/main/java/com/diy/app/infra/servlet/UrlControllerMapper.java @@ -4,6 +4,7 @@ import com.diy.app.business.service.LectureService; import com.diy.app.infra.port.Controller; import com.diy.app.infra.viewRender.ViewResolver; +import com.diy.framework.beans.factory.BeanStorage; import java.util.Comparator; import java.util.Map; @@ -14,7 +15,7 @@ public class UrlControllerMapper { private static UrlControllerMapper instance; public UrlControllerMapper() { - uriToController.put("/lectures", new LectureController(LectureService.getInstance())); + uriToController.put("/lectures", BeanStorage.getInstance().getBean(LectureController.class).get()); } public Controller findController(String uri) { diff --git a/src/main/java/com/diy/app/infra/viewRender/JspView.java b/src/main/java/com/diy/app/infra/viewRender/JspView.java index f5271d55..075fddc8 100644 --- a/src/main/java/com/diy/app/infra/viewRender/JspView.java +++ b/src/main/java/com/diy/app/infra/viewRender/JspView.java @@ -24,6 +24,9 @@ public void render(final HttpServletRequest req, final HttpServletResponse resp, .getFile().split("resources/")[1]; final RequestDispatcher requestDispatcher = req.getRequestDispatcher(filePath); + for (String key : modelAndView.getModel().keySet()) { + req.setAttribute(key, modelAndView.getModel().get(key)); + } requestDispatcher.forward(req, resp); } diff --git a/src/main/java/com/diy/framework/beans/annotations/Autowired.java b/src/main/java/com/diy/framework/beans/annotations/Autowired.java new file mode 100644 index 00000000..3d093de8 --- /dev/null +++ b/src/main/java/com/diy/framework/beans/annotations/Autowired.java @@ -0,0 +1,11 @@ +package com.diy.framework.beans.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.CONSTRUCTOR) +@Retention(RetentionPolicy.RUNTIME) +public @interface Autowired { +} diff --git a/src/main/java/com/diy/framework/beans/annotations/Component.java b/src/main/java/com/diy/framework/beans/annotations/Component.java new file mode 100644 index 00000000..25fe4d20 --- /dev/null +++ b/src/main/java/com/diy/framework/beans/annotations/Component.java @@ -0,0 +1,11 @@ +package com.diy.framework.beans.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Component { +} diff --git a/src/main/java/com/diy/framework/beans/factory/BeanScanner.java b/src/main/java/com/diy/framework/beans/factory/BeanScanner.java new file mode 100644 index 00000000..c4a20479 --- /dev/null +++ b/src/main/java/com/diy/framework/beans/factory/BeanScanner.java @@ -0,0 +1,35 @@ +package com.diy.framework.beans.factory; + +import com.diy.framework.beans.annotations.Autowired; +import org.reflections.Reflections; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.util.*; +import java.util.stream.Collectors; + +public class BeanScanner { + + private final Reflections reflections; + + public BeanScanner(final String... basePackages) { + reflections = new Reflections(basePackages); + } + + public Set> scanClassesTypeAnnotatedWith(final Class annotation) { + return reflections.getTypesAnnotatedWith(annotation) + .stream() + .filter(type -> (!type.isAnnotation() && !type.isInterface())) + .collect(Collectors.toSet()); + } + + public Constructor scanConstructor(final Class clazz) throws NoSuchMethodException { + Optional> autowired = Arrays.stream(clazz.getDeclaredConstructors()) + .filter(constructor -> Objects.nonNull(constructor.getDeclaredAnnotation(Autowired.class))) + .findAny(); + if (autowired.isPresent()) { + return autowired.get(); + } + return clazz.getDeclaredConstructor(); + } +} \ No newline at end of file diff --git a/src/main/java/com/diy/framework/beans/factory/BeanStorage.java b/src/main/java/com/diy/framework/beans/factory/BeanStorage.java new file mode 100644 index 00000000..90582de2 --- /dev/null +++ b/src/main/java/com/diy/framework/beans/factory/BeanStorage.java @@ -0,0 +1,67 @@ +package com.diy.framework.beans.factory; + +import com.diy.framework.beans.annotations.Component; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.*; + +public class BeanStorage { + + private static BeanStorage instance; + + private final Map beans = new HashMap<>(); + private final BeanScanner bc = new BeanScanner("com.diy.app"); + + private BeanStorage() { + Set> classes = bc.scanClassesTypeAnnotatedWith(Component.class); + makeBeans(classes); + } + + public static BeanStorage getInstance() { + if (Objects.isNull(instance)) instance = new BeanStorage(); + return instance; + } + + private void makeBeans(Set> classes) { + for (Class clazz : classes) { + makeBean(clazz); + } + } + + private Object makeBean(Class clazz) { + Constructor constructor = null; + try { + constructor = bc.scanConstructor(clazz); + + Object[] params = Arrays.stream(constructor.getParameterTypes()) + .map(paramType -> findBean(paramType).orElseGet(() -> makeBean(paramType))) + .toArray(); + + constructor.setAccessible(true); + Object bean = constructor.newInstance(params); + beans.put(clazz.getName(), bean); + return bean; + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { + throw new RuntimeException(e); + } finally { + if (constructor != null) { + constructor.setAccessible(false); + } + } + } + + private Optional findBean(final Class classType) { + return beans.values().stream() + .filter(classType::isInstance) + .findAny(); + } + + public Optional getBean(final Class classType) { + return beans.values().stream() + .filter(classType::isInstance) + .map(classType::cast) + .findAny(); + } +} diff --git a/src/main/resources/lecture-list.html b/src/main/resources/lecture-list1.html similarity index 100% rename from src/main/resources/lecture-list.html rename to src/main/resources/lecture-list1.html diff --git a/src/test/java/study/reflection/Car.java b/src/test/java/study/reflection/Car.java new file mode 100644 index 00000000..8977a9fe --- /dev/null +++ b/src/test/java/study/reflection/Car.java @@ -0,0 +1,37 @@ +package study.reflection; + +public class Car { + + private String name; + private int price; + + public String getName() { + return name; + } + + public int getPrice() { + return price; + } + + public Car() { + + } + + public Car(String name, int price) { + this.name = name; + this.price = price; + } + + @PrintView + public void printView() { + System.out.println("자동차 정보를 출력 합니다."); + } + + public String testGetName() { + return "test : " + name; + } + + public String testGetPrice() { + return "test : " + price; + } +} diff --git a/src/test/java/study/reflection/PrintView.java b/src/test/java/study/reflection/PrintView.java new file mode 100644 index 00000000..bbba5b57 --- /dev/null +++ b/src/test/java/study/reflection/PrintView.java @@ -0,0 +1,8 @@ +package study.reflection; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface PrintView { +} diff --git a/src/test/java/study/reflection/ReflectionTest.java b/src/test/java/study/reflection/ReflectionTest.java new file mode 100644 index 00000000..3814a843 --- /dev/null +++ b/src/test/java/study/reflection/ReflectionTest.java @@ -0,0 +1,96 @@ +package study.reflection; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ReflectionTest { + + @Test + @DisplayName("요구사항 1 : Car 객체 정보 가져오기") + void showClass() { + Class carClass = Car.class; + System.out.println(carClass.getName()); + } + + @Test + @DisplayName("요구사항 2 : test로 시작하는 메소드 실행하기") + void executeMethodStartWithTest() throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + Class clazz = Car.class; + Car car = clazz.getDeclaredConstructor(String.class, int.class).newInstance("차 이름", 100000); + + Method[] methods = clazz.getDeclaredMethods(); + Arrays.stream(methods) + .filter(m -> m.getName().startsWith("test")) + .peek(m -> System.out.println(m.getName())) + .forEach(m -> { + try { + System.out.println(m.invoke(car)); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + }); + } + + @Test + @DisplayName("요구사항 3 : @PrintView 애노테이션 메소드 실행하기") + void executeMethodHaveAnnotation() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + Class clazz = Car.class; + Car car = clazz.getDeclaredConstructor(String.class, int.class).newInstance("차 이름", 100000); + + Method[] methods = clazz.getMethods(); + Arrays.stream(methods) + .filter(method -> method.isAnnotationPresent(PrintView.class)) + .forEach(m -> { + try { + m.invoke(car); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + }); + } + + @Test + @DisplayName("요구사항 4 : private field에 값 할당하기") + void setPirvateField() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException { + Class clazz = Car.class; + Car car = clazz.getDeclaredConstructor().newInstance(); + + Field name = clazz.getDeclaredField("name"); + name.setAccessible(true); + Field price = clazz.getDeclaredField("price"); + price.setAccessible(true); + + try { + String nameValue = "이름"; + int priceValue = 1000000; + name.set(car, nameValue); + price.set(car, priceValue); + + assertThat(car.getName()).isEqualTo(nameValue); + assertThat(car.getPrice()).isEqualTo(priceValue); + } finally { + name.setAccessible(false); + price.setAccessible(false); + } + } + + @Test + @DisplayName("요구사항 5 : 인자가 존재하는 인스턴스 생성") + public void constructerWithParameter() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + String nameValue = "이름"; + int priceValue = 1000000; + + Class clazz = Car.class; + Car car = clazz.getDeclaredConstructor(String.class, int.class).newInstance(nameValue, priceValue); + + assertThat(car.getName()).isEqualTo(nameValue); + assertThat(car.getPrice()).isEqualTo(priceValue); + } +} diff --git a/src/test/java/study/reflection/lecture/Lecture.java b/src/test/java/study/reflection/lecture/Lecture.java new file mode 100644 index 00000000..dc7095bd --- /dev/null +++ b/src/test/java/study/reflection/lecture/Lecture.java @@ -0,0 +1,34 @@ +package study.reflection.lecture; + + +public class Lecture { + private final String name; + private final int price; + private boolean visible; + + public Lecture(final String name, final int price) { + this.name = name; + this.price = price; + this.visible = false; + } + + public Lecture(final String name, final int price, final boolean visible) { + this.name = name; + this.price = price; + this.visible = visible; + } + + private void changeVisible() { + this.visible = true; + } + + @MethodOrder(1) + public String getName() { + return this.name; + } + + @MethodOrder(2) + public int getPrice() { + return this.price; + } +} diff --git a/src/test/java/study/reflection/lecture/LectureReflectionTest.java b/src/test/java/study/reflection/lecture/LectureReflectionTest.java new file mode 100644 index 00000000..8c97dbae --- /dev/null +++ b/src/test/java/study/reflection/lecture/LectureReflectionTest.java @@ -0,0 +1,104 @@ +package study.reflection.lecture; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.*; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LectureReflectionTest { + @Test + @DisplayName("요구사항 1 : Lecture 생성자 찾기") + void findConstructor() { + Constructor[] constructors = Lecture.class.getDeclaredConstructors(); + Arrays.stream(constructors) + .forEach(System.out::println); + } + + @Test + @DisplayName("요구사항 2 : Lecture 인스턴스 동적 생성1") + void createConstructor1() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + final String name = "테스트 이름"; + final int price = 10000; + Lecture lecture = Lecture.class.getDeclaredConstructor(String.class, int.class).newInstance(name, price); + + assertThat(lecture.getName()).isEqualTo(name); + assertThat(lecture.getPrice()).isEqualTo(price); + } + + @Test + @DisplayName("요구사항 2 : Lecture 인스턴스 동적 생성2") + void createConstructor2() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + final String name = "테스트 이름"; + final int price = 10000; + final boolean visible = false; + Lecture lecture = Lecture.class.getDeclaredConstructor(String.class, int.class, boolean.class).newInstance(name, price, visible); + + assertThat(lecture.getName()).isEqualTo(name); + assertThat(lecture.getPrice()).isEqualTo(price); + } + + @Test + @DisplayName("요구사항 3 : private 메서드 찾기") + void findPrivateMethod() { + Class clazz = Lecture.class; + Arrays.stream(clazz.getDeclaredMethods()).filter(method -> method.accessFlags().contains(AccessFlag.PRIVATE)).forEach(System.out::println); + } + + @Test + @DisplayName("요구사항 4 : private 메서드 호출") + void executePrivateMethod() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException { + Class clazz = Lecture.class; + + final String name = "테스트 이름"; + final int price = 10000; + final boolean visible = false; + Lecture lecture = clazz.getDeclaredConstructor(String.class, int.class, boolean.class).newInstance(name, price, visible); + + Arrays.stream(clazz.getDeclaredMethods()) + .filter(method -> method.accessFlags().contains(AccessFlag.PRIVATE)) + .forEach(method -> { + try { + method.setAccessible(true); + method.invoke(lecture); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } finally { + method.setAccessible(false); + } + }); + + Field field = clazz.getDeclaredField("visible"); + field.setAccessible(true); + try { + assertThat(field.getBoolean(lecture)).isEqualTo(true); + } finally { + field.setAccessible(false); + } + } + + @Test + @DisplayName("요구사항 5 : 애너테이션으로 메서드 찾기") + void findMethodByAnnotation() { + Method[] methods = Lecture.class.getMethods(); + Arrays.stream(methods) + .filter(method -> method.isAnnotationPresent(MethodOrder.class)) + .forEach(System.out::println); + } + + @Test + @DisplayName("요구사항 6 : @MethodOrder 애너테이션 정보 조회") + void findMethodParameterByAnnotation() { + Method[] methods = Lecture.class.getMethods(); + Arrays.stream(methods) + .filter(method -> method.isAnnotationPresent(MethodOrder.class)) + .forEach(method -> { + System.out.println(method + " : " + method.getAnnotation(MethodOrder.class).value()); + }); + } + +} diff --git a/src/test/java/study/reflection/lecture/MethodOrder.java b/src/test/java/study/reflection/lecture/MethodOrder.java new file mode 100644 index 00000000..239421be --- /dev/null +++ b/src/test/java/study/reflection/lecture/MethodOrder.java @@ -0,0 +1,12 @@ +package study.reflection.lecture; + +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) +public @interface MethodOrder { + int value(); +}