Skip to content

Commit 591e3e0

Browse files
GP - 158: create an exercise about BeanPostProcessor that allows Stri… (#30)
* GP - 158: create an exercise about BeanPostProcessor that allows String auto-trimming
1 parent 82bcb80 commit 591e3e0

File tree

15 files changed

+386
-10
lines changed

15 files changed

+386
-10
lines changed

0-0-intro/0-0-1-welcome-to-java-web-exercises/pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
<plugin>
3939
<groupId>org.springframework.boot</groupId>
4040
<artifactId>spring-boot-maven-plugin</artifactId>
41-
<version>2.4.0</version>
41+
<version>2.7.3</version>
4242
</plugin>
4343
</plugins>
4444
</build>

3-0-spring-framework/3-0-0-hello-spring-framework/pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<dependency>
1616
<groupId>org.springframework</groupId>
1717
<artifactId>spring-context</artifactId>
18-
<version>5.3.6</version>
18+
<version>5.3.23</version>
1919
</dependency>
2020
<dependency>
2121
<groupId>com.bobocode</groupId>

3-0-spring-framework/3-0-1-hello-spring-mvc/pom.xml

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,20 @@
1515
<dependency>
1616
<groupId>org.springframework.boot</groupId>
1717
<artifactId>spring-boot-starter-web</artifactId>
18-
<version>2.4.5</version>
18+
<version>2.7.3</version>
1919
</dependency>
2020

2121
<dependency>
2222
<groupId>org.springframework.boot</groupId>
2323
<artifactId>spring-boot-starter-test</artifactId>
24-
<version>2.4.5</version>
24+
<version>2.7.3</version>
2525
<scope>test</scope>
2626
</dependency>
2727

2828
<dependency>
2929
<groupId>org.springframework.boot</groupId>
3030
<artifactId>spring-boot-starter-thymeleaf</artifactId>
31-
<version>2.4.5</version>
31+
<version>2.7.3</version>
3232
</dependency>
3333

3434
</dependencies>

3-0-spring-framework/3-0-2-view-resolver/pom.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
<dependency>
2121
<groupId>org.springframework</groupId>
2222
<artifactId>spring-webmvc</artifactId>
23-
<version>5.3.6</version>
23+
<version>5.3.23</version>
2424
</dependency>
2525

2626
<dependency>
@@ -34,7 +34,7 @@
3434
<dependency>
3535
<groupId>org.springframework.boot</groupId>
3636
<artifactId>spring-boot-starter-test</artifactId>
37-
<version>2.4.5</version>
37+
<version>2.7.3</version>
3838
<scope>test</scope>
3939
</dependency>
4040

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# <img src="https://raw.githubusercontent.com/bobocode-projects/resources/master/image/logo_transparent_background.png" height=50/>String Trimming Exercise 💪
2+
3+
Improve your *Spring* post-processing and proxy configuration knowledge
4+
5+
### Objectives:
6+
7+
* Implement Spring BeanPostProcessor interface
8+
* Create a bean proxy using CGLib
9+
* Understand the idea of post-processing and proxy creation in Spring
10+
* Explain how `@Enable`... annotations magic works in Spring
11+
12+
### Pre-conditions ❗
13+
14+
You're supposed to be familiar with *Java Fundamentals* and *Spring Core*
15+
16+
### How to start ❓
17+
18+
* Just clone the repository, open class 'TrimmedAnnotationBeanPostProcessor' and start implementing the **todo** section, verify
19+
your changes by running tests
20+
* If you don't have enough knowledge about this domain, check out [this video](https://www.youtube.com/watch?v=SveQOFTEyP4&t=3235s)
21+
* Don't worry if you got stuck, checkout the **exercise/completed** branch and see the final implementation
22+
23+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<parent>
6+
<artifactId>3-0-spring-framework</artifactId>
7+
<groupId>com.bobocode</groupId>
8+
<version>1.0-SNAPSHOT</version>
9+
</parent>
10+
<modelVersion>4.0.0</modelVersion>
11+
12+
<artifactId>3-3-0-enable-string-trimming</artifactId>
13+
<packaging>war</packaging>
14+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.bobocode;
2+
3+
import com.bobocode.annotation.Trimmed;
4+
import org.springframework.beans.factory.config.BeanPostProcessor;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.stereotype.Component;
7+
8+
/**
9+
* This is processor class implements {@link BeanPostProcessor}, looks for a beans where method parameters are marked with
10+
* {@link Trimmed} annotation, creates proxy of them, overrides methods and trims all {@link String} arguments marked with
11+
* {@link Trimmed}. For example if there is a string " Java " as an input parameter it has to be automatically trimmed to "Java"
12+
* if parameter is marked with {@link Trimmed} annotation.
13+
* <p>
14+
*
15+
* Note! This bean is not marked as a {@link Component} to avoid automatic scanning, instead it should be created in
16+
* {@link StringTrimmingConfiguration} class which can be imported to a {@link Configuration} class by annotation
17+
* {@link EnableStringTrimming}
18+
*/
19+
public class TrimmedAnnotationBeanPostProcessor {
20+
//todo: Implement TrimmedAnnotationBeanPostProcessor according to javadoc
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.bobocode.annotation;
2+
3+
/**
4+
* Annotation that can be placed on configuration class to import {@link StringTrimmingConfiguration}
5+
*/
6+
public @interface EnableStringTrimming {
7+
//todo: Implement EnableStringTrimming annotation according to javadoc
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.bobocode.annotation;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
/**
9+
* Annotation that can be placed on {@link String} method parameters to enable automatic trimming values of these parameters.
10+
*/
11+
@Retention(RetentionPolicy.RUNTIME)
12+
@Target(ElementType.PARAMETER)
13+
public @interface Trimmed {
14+
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
package com.bobocode;
2+
3+
import static java.util.stream.Collectors.toList;
4+
import static org.assertj.core.api.Assertions.assertThat;
5+
import static org.junit.jupiter.api.Assertions.assertEquals;
6+
import static org.junit.jupiter.api.Assertions.assertNull;
7+
import static org.junit.jupiter.api.Assertions.assertTrue;
8+
9+
import com.bobocode.config.TrimmingEnabledConfig;
10+
import com.bobocode.config.TrimmingNotEnabledConfig;
11+
import com.bobocode.service.TrimmedService;
12+
import java.lang.annotation.Annotation;
13+
import java.lang.annotation.ElementType;
14+
import java.lang.annotation.Retention;
15+
import java.lang.annotation.RetentionPolicy;
16+
import java.lang.annotation.Target;
17+
import java.lang.reflect.Constructor;
18+
import java.lang.reflect.Method;
19+
import java.util.Arrays;
20+
import java.util.List;
21+
import java.util.NoSuchElementException;
22+
import java.util.Optional;
23+
import java.util.stream.Stream;
24+
import lombok.AllArgsConstructor;
25+
import lombok.EqualsAndHashCode;
26+
import lombok.SneakyThrows;
27+
import org.junit.jupiter.api.DisplayName;
28+
import org.junit.jupiter.api.MethodOrderer;
29+
import org.junit.jupiter.api.Nested;
30+
import org.junit.jupiter.api.Order;
31+
import org.junit.jupiter.api.Test;
32+
import org.junit.jupiter.api.TestMethodOrder;
33+
import org.reflections.Reflections;
34+
import org.reflections.scanners.SubTypesScanner;
35+
import org.springframework.beans.factory.annotation.Autowired;
36+
import org.springframework.beans.factory.config.BeanPostProcessor;
37+
import org.springframework.context.annotation.Bean;
38+
import org.springframework.context.annotation.Configuration;
39+
import org.springframework.context.annotation.Import;
40+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
41+
42+
43+
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
44+
class EnableStringTrimmingTest {
45+
46+
@Test
47+
@Order(1)
48+
@DisplayName("Annotation @EnableStringTrimming has target 'TYPE'")
49+
void enableStringTrimmingAnnotationHasCorrectTarget() {
50+
Optional<Class<? extends Annotation>> annotation = findAnnotationInPackage("EnableStringTrimming");
51+
Optional<ElementType[]> targetValues = annotation.map(a -> a.getAnnotation(Target.class)).map(Target::value);
52+
53+
assertTrue(targetValues.isPresent());
54+
assertEquals(1, targetValues.get().length);
55+
assertThat(List.of(targetValues.get())).contains(ElementType.TYPE);
56+
57+
}
58+
59+
@Test
60+
@Order(2)
61+
@DisplayName("Annotation @EnableStringTrimming has retention 'RUNTIME'")
62+
void enableStringTrimmingAnnotationHasRuntimeRetention() {
63+
Optional<Class<? extends Annotation>> annotation = findAnnotationInPackage("EnableStringTrimming");
64+
Optional<Retention> retention = annotation.map(a -> a.getAnnotation(Retention.class))
65+
.filter(r -> r.value().equals(RetentionPolicy.RUNTIME));
66+
67+
assertTrue(retention.isPresent());
68+
}
69+
70+
@Test
71+
@Order(3)
72+
@DisplayName("Annotation @EnableStringTrimming imports StringTrimmingConfiguration")
73+
void enableStringTrimmingAnnotationImportsCorrectConfig() {
74+
Optional<Class<? extends Annotation>> annotation = findAnnotationInPackage("EnableStringTrimming");
75+
List<Class<?>> importValues = Arrays.stream(annotation
76+
.map(a -> a.getAnnotation(Import.class))
77+
.map(Import::value)
78+
.orElseGet(() -> new Class<?>[0]))
79+
.toList();
80+
List<String> importNames = importValues.stream().map(Class::getSimpleName).collect(toList());
81+
assertThat(importNames).contains("StringTrimmingConfiguration");
82+
}
83+
84+
@Test
85+
@Order(4)
86+
@DisplayName("StringTrimmingConfiguration is implemented")
87+
void stringTrimmingConfiguration() {
88+
Optional<Class<?>> configClass = findClassInPackage("StringTrimmingConfiguration");
89+
assertTrue(configClass.isPresent());
90+
}
91+
92+
@Test
93+
@Order(5)
94+
@DisplayName("StringTrimmingConfiguration is not annotated by @Configuration Spring annotation.")
95+
void stringTrimmingConfigurationIsNotAnnotatedAsConfiguration() {
96+
Optional<Class<?>> configClass = findClassInPackage("StringTrimmingConfiguration");
97+
assertNull(configClass.get().getDeclaredAnnotation(Configuration.class));
98+
}
99+
100+
@Test
101+
@Order(6)
102+
@DisplayName("StringTrimmingConfiguration declares a TrimmedAnnotationBeanPostProcessor bean")
103+
void stringTrimmingConfigurationCreatesBeanOfTrimmedAnnotationPostProcessor() {
104+
Optional<Class<?>> configClass = findClassInPackage("StringTrimmingConfiguration");
105+
106+
Method[] methods =
107+
configClass.orElseThrow(() -> new NoSuchElementException("StringTrimmingConfiguration class is not found"))
108+
.getDeclaredMethods();
109+
110+
Optional<Method> postProcessorBeanMethod =
111+
Stream.of(methods).filter(m -> m.getReturnType().equals(TrimmedAnnotationBeanPostProcessor.class))
112+
.findFirst();
113+
assertTrue(postProcessorBeanMethod.isPresent());
114+
}
115+
116+
@Test
117+
@Order(7)
118+
@DisplayName("Methods of StringTrimmingConfiguration that provides TrimmedAnnotationBeanPostProcessor instance annotated with" +
119+
" @Bean annotation")
120+
void trimmedAnnotationBeanPostProcessorMethodMarkedAsBean() {
121+
Optional<Class<?>> configClass = findClassInPackage("StringTrimmingConfiguration");
122+
Method[] methods = configClass.orElseThrow(
123+
() -> new NoSuchElementException("StringTrimmingConfiguration class is not found")).getDeclaredMethods();
124+
Optional<Bean> beanAnnotation = Stream.of(methods)
125+
.filter(m -> m.getReturnType().equals(TrimmedAnnotationBeanPostProcessor.class))
126+
.map(m -> m.getAnnotation(Bean.class)).findFirst();
127+
assertTrue(beanAnnotation.isPresent());
128+
}
129+
130+
@Test
131+
@Order(8)
132+
@DisplayName("TrimmedAnnotationBeanPostProcessor implements BeanPostProcessor interface")
133+
void trimmedAnnotationBeanPostProcessorImplementsBeanPostProcessor() {
134+
boolean isBeanPostProcessorPresent = Arrays.stream(TrimmedAnnotationBeanPostProcessor.class.getInterfaces())
135+
.anyMatch(i -> i.isAssignableFrom(
136+
BeanPostProcessor.class));
137+
assertTrue(isBeanPostProcessorPresent);
138+
}
139+
140+
@Test
141+
@Order(9)
142+
@DisplayName("TrimmedAnnotationBeanPostProcessor overrides postProcessAfterInitialization() method")
143+
void trimmedAnnotationBeanPostProcessorOverridesPostProcessAfterInitMethod() {
144+
List<String> methodNames = Arrays.stream(TrimmedAnnotationBeanPostProcessor.class.getDeclaredMethods())
145+
.map(Method::getName)
146+
.toList();
147+
assertThat(methodNames).contains("postProcessAfterInitialization");
148+
}
149+
150+
@Test
151+
@Order(10)
152+
@DisplayName("Method postProcessBeforeInitialization() does not create a new proxy")
153+
@SneakyThrows
154+
void trimmedAnnotationBeanPostProcessorNotOverridesPostProcessAfterInitMethod() {
155+
Constructor<TrimmedAnnotationBeanPostProcessor> constructor = TrimmedAnnotationBeanPostProcessor.class.getDeclaredConstructor();
156+
TrimmedAnnotationBeanPostProcessor processor = constructor.newInstance();
157+
TestBean testBean = new TestBean(1L, "Bobobean");
158+
159+
Method method = null;
160+
try {
161+
method = processor.getClass().getDeclaredMethod(
162+
"postProcessBeforeInitialization", Object.class, String.class);
163+
} catch (NoSuchMethodException e) {
164+
165+
Optional<Class<?>> beanPostProcessor = Arrays.stream(processor.getClass().getInterfaces())
166+
.filter(i -> i.getSimpleName().equals("BeanPostProcessor")).findFirst();
167+
168+
if (beanPostProcessor.isPresent()) {
169+
method = beanPostProcessor.get().getDeclaredMethod(
170+
"postProcessBeforeInitialization", Object.class, String.class);
171+
}
172+
}
173+
Object beanAfterPostProcessing = method.invoke(processor, testBean, testBean.getClass().getName());
174+
175+
assertEquals(beanAfterPostProcessing, testBean);
176+
}
177+
178+
@Nested
179+
@SpringJUnitConfig(classes = {TrimmingEnabledConfig.class})
180+
class TrimmingEnabledTest {
181+
182+
@Test
183+
@DisplayName("TrimmedAnnotationBeanPostProcessor trims String input params marked with @Trimmed")
184+
void trimmedAnnotationPostProcessorAnnotatedInputParams(@Autowired TrimmedService service) {
185+
String inputArgs = " Simba Bimba ";
186+
String actual = service.getTheTrimmedString(inputArgs);
187+
188+
assertEquals(inputArgs.trim(), actual);
189+
}
190+
191+
@Test
192+
@DisplayName("TrimmedAnnotationBeanPostProcessor not trims String input params that not marked by @Trimmed")
193+
void trimmedAnnotationPostProcessorNotAnnotatedInputParams(@Autowired TrimmedService service) {
194+
String inputArgs = " Simba Bimba ";
195+
String actual = service.getNotTrimmedString(inputArgs);
196+
197+
assertEquals(inputArgs, actual);
198+
}
199+
}
200+
201+
@Nested
202+
@SpringJUnitConfig(classes = TrimmingNotEnabledConfig.class)
203+
class TrimmingNotEnabledTest {
204+
205+
@Test
206+
@DisplayName(
207+
"TrimmedAnnotationBeanPostProcessor does not trims String input parameters marked by " +
208+
"@Trimmed if trimming is not enabled by @EnableStringTrimming")
209+
void trimmedAnnotationPostProcessorAnnotatedInputParams(@Autowired TrimmedService service) {
210+
String inputArgs = " Mufasa LionKing ";
211+
String actual = service.getTheTrimmedString(inputArgs);
212+
213+
assertEquals(inputArgs, actual);
214+
}
215+
}
216+
217+
private Optional<Class<?>> findClassInPackage(String className) {
218+
String packageName = TrimmedAnnotationBeanPostProcessor.class.getPackageName();
219+
Reflections reflections = new Reflections(packageName, new SubTypesScanner(false));
220+
return reflections.getSubTypesOf(Object.class)
221+
.stream()
222+
.filter(c -> c.getSimpleName().equals(className))
223+
.findFirst();
224+
}
225+
226+
private Optional<Class<? extends Annotation>> findAnnotationInPackage(String annotationName) {
227+
String packageName = TrimmedAnnotationBeanPostProcessor.class.getPackageName();
228+
Reflections reflections = new Reflections(packageName, new SubTypesScanner(false));
229+
return reflections.getSubTypesOf(Annotation.class)
230+
.stream()
231+
.filter(a -> a.getSimpleName().equals(annotationName))
232+
.findFirst();
233+
}
234+
235+
@EqualsAndHashCode
236+
@AllArgsConstructor
237+
private class TestBean {
238+
239+
private Long id;
240+
private String name;
241+
242+
@Override
243+
public String toString() {
244+
return "TestBean{" +
245+
"id=" + id +
246+
", name='" + name + '\'' +
247+
'}';
248+
}
249+
}
250+
}

0 commit comments

Comments
 (0)