diff --git a/3-0-spring-framework/bean-post-processor/README.MD b/3-0-spring-framework/bean-post-processor/README.MD new file mode 100644 index 0000000..25a9dbc --- /dev/null +++ b/3-0-spring-framework/bean-post-processor/README.MD @@ -0,0 +1,38 @@ +# Bean Post Processor exercise +Improve your *Spring* advanced skills 💪 +### Task +Implement automatic String trimming functionality. So if I pass " Hello " as an argument to my bean’s method, or return it from the bean’s method, it gets automatically trimmed to "Hello". + +- create annotation @Trimmed that you can put on class + +- create annotation @EnableStringTrimming that will enable automatic String trimming for all beans that are annotated with @Trimmed + +- create TrimmedAnnotationBeanPostProcessor that will check for beans that are marked with @Trimmed, + +- create a proxy of those classes, and override methods. Proxy methods should: + + - trim all String arguments + + - trim all String return values + +- extract TrimmedAnnotationBeanPostProcessor into a separate StringTrimmingConfiguration that is imported by @EnableStringTrimming + +To verify your configuration, run `BeanPostProcessorTest.java` + + +### Pre-conditions ❗ +You're supposed to be familiar with *Spring* + +### How to start ❓ +* Just clone the repository and start implementing the **todo** section, verify your changes by running tests +* If you don't have enough knowledge about this domain, check out the [links below](#Related-materials-ℹ) +* Don't worry if you got stuck, checkout the **exercise/completed** branch and see the final implementation + +### Related materials ℹ + todo + +--- +#### 🆕 First time here? – [See Introduction](https://github.com/bobocode-projects/java-fundamentals-course/tree/main/0-0-intro#introduction) + +## +
\ No newline at end of file diff --git a/3-0-spring-framework/bean-post-processor/pom.xml b/3-0-spring-framework/bean-post-processor/pom.xml new file mode 100644 index 0000000..dddb1d2 --- /dev/null +++ b/3-0-spring-framework/bean-post-processor/pom.xml @@ -0,0 +1,27 @@ + + + + 3-0-spring-framework + com.bobocode + 1.0-SNAPSHOT + + 4.0.0 + + bean-post-processor + + + 11 + 11 + + + + + cglib + cglib + 3.3.0 + + + + \ No newline at end of file diff --git a/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/annotation/EnableStringTrimming.java b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/annotation/EnableStringTrimming.java new file mode 100644 index 0000000..4ae5384 --- /dev/null +++ b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/annotation/EnableStringTrimming.java @@ -0,0 +1,14 @@ +package com.bobocode.annotation; + +import com.bobocode.util.StringTrimmingConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@Configuration +@Import(StringTrimmingConfiguration.class) +public @interface EnableStringTrimming { +} diff --git a/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/annotation/Trimmed.java b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/annotation/Trimmed.java new file mode 100644 index 0000000..ff1c423 --- /dev/null +++ b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/annotation/Trimmed.java @@ -0,0 +1,8 @@ +package com.bobocode.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface Trimmed { +} diff --git a/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/config/ApplicationConfig.java b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/config/ApplicationConfig.java new file mode 100644 index 0000000..ead5c5b --- /dev/null +++ b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/config/ApplicationConfig.java @@ -0,0 +1,16 @@ +package com.bobocode.config; + +import com.bobocode.annotation.EnableStringTrimming; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * Configure the application for using @Trimmed annotation and corresponding logic. + */ + +@Configuration +@ComponentScan(basePackages = "com.bobocode.service") +@EnableStringTrimming +public class ApplicationConfig { + +} diff --git a/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/service/TextService.java b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/service/TextService.java new file mode 100644 index 0000000..549e43d --- /dev/null +++ b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/service/TextService.java @@ -0,0 +1,23 @@ +package com.bobocode.service; + +import com.bobocode.annotation.Trimmed; +import org.springframework.stereotype.Service; + +/** + * Configure the service to provide trimming process. + */ + +@Service +@Trimmed +public class TextService { + public String savedText; + private final static String AVAILABLE_TEXT = " Who cares about tabbing? "; + + public void saveText(String text){ + savedText = text; + } + + public String getAvailableText(){ + return AVAILABLE_TEXT; + } +} diff --git a/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/util/StringTrimmingConfiguration.java b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/util/StringTrimmingConfiguration.java new file mode 100644 index 0000000..f566114 --- /dev/null +++ b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/util/StringTrimmingConfiguration.java @@ -0,0 +1,14 @@ +package com.bobocode.util; + +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class StringTrimmingConfiguration { + + @Bean + public BeanPostProcessor trimmedAnnotationBeanPostProcessor(){ + return new TrimmedAnnotationBeanPostProcessor(); + } +} diff --git a/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/util/TrimmedAnnotationBeanPostProcessor.java b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/util/TrimmedAnnotationBeanPostProcessor.java new file mode 100644 index 0000000..22c143d --- /dev/null +++ b/3-0-spring-framework/bean-post-processor/src/main/java/com/bobocode/util/TrimmedAnnotationBeanPostProcessor.java @@ -0,0 +1,42 @@ +package com.bobocode.util; + +import com.bobocode.annotation.Trimmed; +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; + +import java.lang.reflect.Method; +import java.util.Arrays; + +public class TrimmedAnnotationBeanPostProcessor implements BeanPostProcessor { + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + Class beanClass = bean.getClass(); + if (beanClass.isAnnotationPresent(Trimmed.class)) { + return createTrimmedProxy(beanClass); + } + return bean; + } + + private Object createTrimmedProxy(Class beanClass) { + var enhancer = new Enhancer(); + enhancer.setSuperclass(beanClass); + enhancer.setInterfaces(beanClass.getInterfaces()); + MethodInterceptor interceptor = (Object obj, Method method, Object[] args, MethodProxy proxy) -> { + Object[] arguments = Arrays.stream(args) + .filter(arg -> arg instanceof String) + .map(arg -> ((String) arg).trim()) + .toArray(); + + if (method.getReturnType().equals(String.class)) { + String invokeSuper = (String) proxy.invokeSuper(obj, arguments); + return (String) invokeSuper.trim(); + } + return proxy.invokeSuper(obj, arguments); + }; + enhancer.setCallback(interceptor); + return beanClass.cast(enhancer.create()); + } +} diff --git a/3-0-spring-framework/bean-post-processor/src/test/java/com/bobocode/BeanPostProcessorTest.java b/3-0-spring-framework/bean-post-processor/src/test/java/com/bobocode/BeanPostProcessorTest.java new file mode 100644 index 0000000..80993d3 --- /dev/null +++ b/3-0-spring-framework/bean-post-processor/src/test/java/com/bobocode/BeanPostProcessorTest.java @@ -0,0 +1,289 @@ +package com.bobocode; + +import com.bobocode.config.ApplicationConfig; +import com.bobocode.service.TextService; +import com.bobocode.testService.NotTrimmedTextService; +import lombok.SneakyThrows; +import org.junit.jupiter.api.*; +import org.reflections.Reflections; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringJUnitConfig(BeanPostProcessorTest.TestConfig.class) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class BeanPostProcessorTest { + @Configuration + @ComponentScan(basePackages = "com.bobocode") + static class TestConfig { + } + + @Autowired + TextService textService; + + @Autowired + NotTrimmedTextService notTrimmedTextService; + + private static final String ANNOTATIONS_PACKAGE = "com.bobocode.annotation"; + private static final String UTIL_PACKAGE = "com.bobocode.util"; + private static final String TRIMMED = "Trimmed"; + private static final String ENABLE_STRING_TRIMMING = "EnableStringTrimming"; + private static final String TRIMMED_ANNOTATION_BEAN_POST_PROCESSOR = "TrimmedAnnotationBeanPostProcessor"; + private static final String STRING_TRIMMING_CONFIGURATION = "StringTrimmingConfiguration"; + + private Reflections annotationReflections; + private Reflections configurationReflections; + + @BeforeEach + @SneakyThrows + public void init() { + annotationReflections = new Reflections(ANNOTATIONS_PACKAGE); + configurationReflections = new Reflections(UTIL_PACKAGE); + } + + @Test + @Order(1) + @DisplayName("@Trimmed annotation exists") + void trimmedAnnotationExists() throws ClassNotFoundException { + Class.forName(ANNOTATIONS_PACKAGE + "." + TRIMMED); + } + + @Test + @Order(2) + @DisplayName("Trimmed annotation class is marked by @Retention with \"Runtime\" policy") + void trimmedAnnotationClassIsMarkedByRetentionRuntimePolicy() { + Optional> annotationClass = getAnnotationClassMarketByRetentionRuntimePolicy(TRIMMED); + + assertThat(annotationClass).isPresent(); + } + + @Test + @Order(3) + @DisplayName("@EnableStringTrimming annotation exists") + void enableStringTrimmingAnnotationExists() throws ClassNotFoundException { + Class.forName(ANNOTATIONS_PACKAGE + "." + ENABLE_STRING_TRIMMING); + } + + @Test + @Order(4) + @DisplayName("EnableStringTrimming annotation class is marked by @Retention with \"Runtime\" policy") + void enableStringTrimmingAnnotationClassIsMarkedByRetentionRuntimePolicy() { + Optional> annotationClass = getAnnotationClassMarketByRetentionRuntimePolicy(ENABLE_STRING_TRIMMING); + + assertThat(annotationClass).isPresent(); + } + + @Test + @Order(5) + @DisplayName("ApplicationConfig class is marked by @Configuration annotation") + void applicationConfigurationClassExists() { + Configuration annotation = ApplicationConfig.class.getAnnotation(Configuration.class); + + assertThat(annotation).isNotNull(); + } + + @Test + @Order(6) + @DisplayName("Package scanning is configured for service package") + void applicationComponentScanIsConfiguredForService() { + ComponentScan annotation = ApplicationConfig.class.getAnnotation(ComponentScan.class); + + Optional scannedPackage = Arrays.stream(annotation.basePackages()) + .filter(p -> p.equals("com.bobocode.service")) + .findAny(); + assertThat(annotation).isNotNull(); + assertThat(scannedPackage).isPresent(); + } + + @Test + @Order(7) + @DisplayName("TextService is marked by @Service annotation") + void textServiceIcMarkedAsComponent() { + Service annotation = TextService.class.getAnnotation(Service.class); + + assertThat(annotation).isNotNull(); + } + + @Test + @Order(8) + @DisplayName("TextService is marked by @Trimmed annotation") + void textServiceIcMarkedAsTrimmed() { + Annotation[] annotations = TextService.class.getAnnotations(); + + Optional anyTrimmedAnnotation = Arrays.stream(annotations) + .map(a -> a.annotationType().getSimpleName()) + .filter(n -> n.equals(TRIMMED)) + .findAny(); + + assertThat(anyTrimmedAnnotation).isPresent(); + } + + @Test + @Order(9) + @DisplayName("TrimmedAnnotationBeanPostProcessor class exists in util package") + void trimmedAnnotationBeanPostProcessorClassExists() throws ClassNotFoundException { + Class.forName(UTIL_PACKAGE + "." + TRIMMED_ANNOTATION_BEAN_POST_PROCESSOR); + } + + @Test + @Order(10) + @DisplayName("TrimmedAnnotationBeanPostProcessor class implements BeanPostProcessor interface") + void trimmedAnnotationBeanPostProcessorClassImplementsBeanPostProcessor() { + Set> beanPostProcessorsClasses = configurationReflections + .getSubTypesOf(BeanPostProcessor.class); + + Optional anyTrimmedBeanPostProcessor = beanPostProcessorsClasses.stream() + .map(Class::getSimpleName) + .filter(n -> n.equals(TRIMMED_ANNOTATION_BEAN_POST_PROCESSOR)) + .findAny(); + + assertThat(anyTrimmedBeanPostProcessor).isPresent(); + } + + @Test + @Order(11) + @DisplayName("TrimmedAnnotationBeanPostProcessor class is not marked as Spring bean") + void trimmedAnnotationBeanPostProcessorClassIsNotMarkedAsBean() { + Set> beanPostProcessorsClasses = configurationReflections + .getSubTypesOf(BeanPostProcessor.class); + + Optional> anyTrimmedBeanPostProcessor = beanPostProcessorsClasses.stream() + .filter(c -> c.getSimpleName().equals(TRIMMED_ANNOTATION_BEAN_POST_PROCESSOR)) + .findAny(); + + boolean componentAnnotationIsPresent = anyTrimmedBeanPostProcessor + .orElseThrow().isAnnotationPresent(Component.class); + boolean serviceAnnotationIsPresent = anyTrimmedBeanPostProcessor + .orElseThrow().isAnnotationPresent(Service.class); + + assertFalse(componentAnnotationIsPresent); + assertFalse(serviceAnnotationIsPresent); + } + + @Test + @Order(12) + @DisplayName("StringTrimmedConfiguration class exists in \"util\" package") + void stringTrimmedConfigurationClassExists() throws ClassNotFoundException { + Class.forName(UTIL_PACKAGE + "." + STRING_TRIMMING_CONFIGURATION); + } + + @Test + @Order(13) + @DisplayName("StringTrimmedConfiguration class is marked by @Configuration annotation") + void stringTrimmedConfigurationClassIsMarkedByConfiguration() { + Optional> anyMarkedClass = configurationReflections.getTypesAnnotatedWith(Configuration.class).stream() + .filter(c -> c.getSimpleName().equals(STRING_TRIMMING_CONFIGURATION)) + .findAny(); + + assertThat(anyMarkedClass).isPresent(); + } + + @Test + @Order(14) + @DisplayName("StringTrimmedConfiguration class creates TrimmedAnnotationBeanPostProcessor bean using method") + void stringTrimmedConfigurationClassCreatesTrimmedBeanPostProcessor() throws ClassNotFoundException { + Class configurationClass = Class.forName(UTIL_PACKAGE + "." + STRING_TRIMMING_CONFIGURATION); + + Optional optionalMethod = Arrays + .stream(configurationClass.getDeclaredMethods()) + .filter(method -> method.isAnnotationPresent(Bean.class)) + .filter(method -> method.getReturnType().equals(BeanPostProcessor.class)) + .findAny(); + + assertThat(optionalMethod).isPresent(); + } + + @Test + @Order(15) + @DisplayName("EnableStringAnnotation class is marked with @Configuration") + void enableStringTrimmingAnnotationClassIsMarkedAsConfiguration() { + Optional> anyAnnotationClass = annotationReflections + .getTypesAnnotatedWith(Configuration.class).stream() + .filter(c -> c.getSimpleName().equals(ENABLE_STRING_TRIMMING)) + .findAny(); + + assertThat(anyAnnotationClass).isPresent(); + } + + @Test + @Order(16) + @DisplayName("EnableStringAnnotation class is marked with @Configuration") + void enableStringTrimmingAnnotationClassIsMarkedWithConfiguredImport() { + Optional> anyAnnotationClass = annotationReflections + .getTypesAnnotatedWith(Import.class).stream() + .filter(c -> c.getSimpleName().equals(ENABLE_STRING_TRIMMING)) + .filter(c -> c.getAnnotation(Import.class).value()[0].getSimpleName().equals(STRING_TRIMMING_CONFIGURATION)) + .findAny(); + + assertThat(anyAnnotationClass).isPresent(); + } + + @Test + @Order(17) + @DisplayName("TextService bean is CGLIB proxy") + void textServiceHasProxy() { + String className = textService.getClass().getSimpleName(); + + boolean isCGLibCreature = className.contains("CGLIB"); + + assertThat(className).isNotEqualTo("TextService"); + assertTrue(isCGLibCreature); + } + + @Test + @Order(18) + @DisplayName("Bean is not proxy when @Trimmed annotation is missed") + void beanIsNotProxyWhenTrimmedAnnotationIsNotUsed() { + String name = notTrimmedTextService.getClass().getSimpleName(); + + assertThat(name).isEqualTo("NotTrimmedTextService"); + } + + @Test + @Order(19) + @DisplayName("TextService receives trimmed String method arguments") + void textServiceReceivesTrimmedStringMethodArguments() { + String text = " Need more space "; + textService.saveText(text); + + String result = textService.savedText; + + assertThat(result).isEqualTo("Need more space"); + } + + @Test + @Order(20) + @DisplayName("TextService method returns trimmed String value") + void textServiceMethodReturnsTrimmedStringValue() { + String result = textService.getAvailableText(); + + assertThat(result).isEqualTo("Who cares about tabbing?"); + } + + private Optional> getAnnotationClassMarketByRetentionRuntimePolicy(String className) { + Set> annotations = + annotationReflections.getTypesAnnotatedWith(Retention.class); + return annotations.stream() + .filter(c -> c.getSimpleName().equals(className)) + .filter(c -> c.getAnnotation(Retention.class).value().equals(RetentionPolicy.RUNTIME)) + .findAny(); + } +} diff --git a/3-0-spring-framework/bean-post-processor/src/test/java/com/bobocode/testService/NotTrimmedTextService.java b/3-0-spring-framework/bean-post-processor/src/test/java/com/bobocode/testService/NotTrimmedTextService.java new file mode 100644 index 0000000..3bc848a --- /dev/null +++ b/3-0-spring-framework/bean-post-processor/src/test/java/com/bobocode/testService/NotTrimmedTextService.java @@ -0,0 +1,7 @@ +package com.bobocode.testService; + +import org.springframework.stereotype.Service; + +@Service +public class NotTrimmedTextService { +} diff --git a/3-0-spring-framework/pom.xml b/3-0-spring-framework/pom.xml index 6beb492..3a73db4 100644 --- a/3-0-spring-framework/pom.xml +++ b/3-0-spring-framework/pom.xml @@ -18,6 +18,7 @@ 3-1-1-dispatcher-servlet-initializer 3-0-2-view-resolver 3-2-1-account-rest-api + bean-post-processor diff --git a/pom.xml b/pom.xml index a006708..5884de2 100644 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,7 @@ org.reflections reflections - 0.9.12 + 0.10.2 org.hamcrest