diff --git a/3-0-spring-framework/3-0-0-hello-spring-framework/README.MD b/3-0-spring-framework/3-0-0-hello-spring-framework/README.MD new file mode 100644 index 0000000..ff8f127 --- /dev/null +++ b/3-0-spring-framework/3-0-0-hello-spring-framework/README.MD @@ -0,0 +1,21 @@ +# Hello ApplicationContext exercise :muscle: +Improve your *Spring ApplicationContext* Java configuration skills +### Task +The task is to **configure `ApplicationContext`** that contains `AccountService` bean, `AccountDao` bean +and `TestDataGenerator` bean. Your job is to follow the instructions in the *todo* section and **implement +a proper configuration.** + +To verify your configuration, run `AppConfigTest.java` + + +### Pre-conditions :heavy_exclamation_mark: +You're supposed to be familiar with *Spring IoC* and *Dependency injection* + +### How to start :question: +* 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-information_source) +* Don't worry if you got stuck, checkout the **exercise/completed** branch and see the final implementation + +### Related materials :information_source: + * [Spring IoC basics tutorial](https://github.com/boy4uck/spring-framework-tutorial/tree/master/spring-framework-ioc-basics) + diff --git a/3-0-spring-framework/3-0-0-hello-spring-framework/pom.xml b/3-0-spring-framework/3-0-0-hello-spring-framework/pom.xml new file mode 100644 index 0000000..0dd36ae --- /dev/null +++ b/3-0-spring-framework/3-0-0-hello-spring-framework/pom.xml @@ -0,0 +1,49 @@ + + + + 3-0-spring-framework + com.bobocode + 1.0-SNAPSHOT + + 4.0.0 + + 3-0-0-hello-spring-framework + + + + org.springframework + spring-context + 5.2.12.RELEASE + + + org.springframework + spring-test + 5.2.12.RELEASE + + + com.bobocode + spring-framework-exercises-util + 1.0-SNAPSHOT + + + org.hamcrest + hamcrest-all + 1.3 + test + + + org.slf4j + slf4j-simple + 1.7.24 + + + com.bobocode + spring-framework-exercises-model + 1.0-SNAPSHOT + compile + + + + \ No newline at end of file diff --git a/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/config/ApplicationConfig.java b/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/config/ApplicationConfig.java new file mode 100644 index 0000000..91df5b3 --- /dev/null +++ b/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/config/ApplicationConfig.java @@ -0,0 +1,30 @@ +package com.bobocode.config; + +import com.bobocode.TestDataGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.ImportResource; +import org.springframework.stereotype.Component; + +/** + * This class specify application context configuration. Basically, it's all about which instances of which classes + * should be created and registered in the context. An instance that is registered in the context is called 'bean'. + *

+ * To tell the container, which bean should be created, you could either specify packages to scan using @{@link ComponentScan}, + * or declare your own beans using @{@link Bean}. When you use @{@link ComponentScan} the container will discover + * specified package and its sub-folders, to find all classes marked @{@link Component}. + *

+ * If you want to import other configs from Java class or XML file, you could use @{@link Import} + * and @{@link ImportResource} accordingly + *

+ * todo 1: make this class a Spring configuration class + * todo 2: enable component scanning for dao and service packages + * todo 3: provide explicit configuration for a bean of type {@link TestDataGenerator} with name "dataGenerator" in this class. + * hint: use method creation approach with @Bean annotation; + * todo 4: Don't specify bean name "dataGenerator" explicitly + */ + +public class ApplicationConfig { + +} diff --git a/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/dao/AccountDao.java b/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/dao/AccountDao.java new file mode 100644 index 0000000..cbc888b --- /dev/null +++ b/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/dao/AccountDao.java @@ -0,0 +1,12 @@ +package com.bobocode.dao; + +import com.bobocode.model.Account; + +import java.util.List; + +/** + * Defines an API for {@link Account} data access object (DAO). + */ +public interface AccountDao { + List findAll(); +} diff --git a/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/dao/FakeAccountDao.java b/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/dao/FakeAccountDao.java new file mode 100644 index 0000000..9543845 --- /dev/null +++ b/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/dao/FakeAccountDao.java @@ -0,0 +1,34 @@ +package com.bobocode.dao; + +import com.bobocode.TestDataGenerator; +import com.bobocode.model.Account; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toList; + +/** + * This class should be marked with @{@link Component}, thus Spring container will create an instance + * of {@link FakeAccountDao} class, and will register it the context. + *

+ * todo: configure this class as Spring component with bean name "accountDao" + * todo: use explicit (with {@link Autowired} annotation) constructor-based dependency injection for specific bean + */ + +public class FakeAccountDao implements AccountDao { + private List accounts; + + public FakeAccountDao(TestDataGenerator testDataGenerator) { + this.accounts = Stream.generate(testDataGenerator::generateAccount) + .limit(20) + .collect(toList()); + } + + @Override + public List findAll() { + return accounts; + } +} diff --git a/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/service/AccountService.java b/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/service/AccountService.java new file mode 100644 index 0000000..3bc0b66 --- /dev/null +++ b/3-0-spring-framework/3-0-0-hello-spring-framework/src/main/java/com/bobocode/service/AccountService.java @@ -0,0 +1,29 @@ +package com.bobocode.service; + +import com.bobocode.dao.AccountDao; +import com.bobocode.model.Account; + +import java.util.Comparator; +import java.util.List; + +/** + * Provides service API for {@link Account}. + *

+ * todo: configure {@link AccountService} bean implicitly using special annotation for service classes + * todo: use implicit constructor-based dependency injection (don't use {@link org.springframework.beans.factory.annotation.Autowired}) + */ + +public class AccountService { + private final AccountDao accountDao; + + public AccountService(AccountDao accountDao) { + this.accountDao = accountDao; + } + + public Account findRichestAccount() { + List accounts = accountDao.findAll(); + return accounts.stream() + .max(Comparator.comparing(Account::getBalance)) + .get(); + } +} diff --git a/3-0-spring-framework/3-0-0-hello-spring-framework/src/test/java/com/bobocode/ApplicationConfigTest.java b/3-0-spring-framework/3-0-0-hello-spring-framework/src/test/java/com/bobocode/ApplicationConfigTest.java new file mode 100644 index 0000000..2c7a211 --- /dev/null +++ b/3-0-spring-framework/3-0-0-hello-spring-framework/src/test/java/com/bobocode/ApplicationConfigTest.java @@ -0,0 +1,120 @@ +package com.bobocode; + +import com.bobocode.config.ApplicationConfig; +import com.bobocode.dao.AccountDao; +import com.bobocode.dao.FakeAccountDao; +import com.bobocode.service.AccountService; +import org.junit.jupiter.api.*; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ApplicationConfigTest { + + @Test + @Order(1) + @DisplayName("Config class is marked as @Configuration") + void configClassIsMarkedAsConfiguration() { + Configuration configuration = ApplicationConfig.class.getAnnotation(Configuration.class); + + assertNotNull(configuration); + } + + @Test + @Order(2) + @DisplayName("@ComponentScan is enabled") + void componentScanIsEnabled() { + ComponentScan componentScan = ApplicationConfig.class.getAnnotation(ComponentScan.class); + + assertNotNull(componentScan); + } + + @Test + @Order(3) + @DisplayName("@ComponentScan configured for \"dao\" and \"service\" packages") + void componentScanPackagesAreSpecified() { + ComponentScan componentScan = ApplicationConfig.class.getAnnotation(ComponentScan.class); + String[] packages = componentScan.basePackages(); + if (packages.length == 0) { + packages = componentScan.value(); + } + assertThat(packages).containsExactlyInAnyOrder("com.bobocode.dao", "com.bobocode.service"); + } + + @Test + @Order(4) + @DisplayName("DataGenerator bean is configured in method marked with @Bean") + void dataGeneratorBeanIsConfiguredExplicitly() { + Method[] methods = ApplicationConfig.class.getMethods(); + Method testDataGeneratorBeanMethod = findTestDataGeneratorBeanMethod(methods); + + assertNotNull(testDataGeneratorBeanMethod); + } + + @Test + @Order(5) + @DisplayName("DataGenerator bean name is not specified explicitly") + void dataGeneratorBeanNameIsNotSpecifiedExplicitly() { + Method[] methods = ApplicationConfig.class.getMethods(); + Method testDataGeneratorBeanMethod = findTestDataGeneratorBeanMethod(methods); + Bean bean = testDataGeneratorBeanMethod.getDeclaredAnnotation(Bean.class); + + assertThat(bean.name().length).isEqualTo(0); + assertThat(bean.value().length).isEqualTo(0); + } + + @Test + @Order(6) + @DisplayName("FakeAccountDao is configured as @Component") + void fakeAccountDaoIsConfiguredAsComponent() { + Component component = FakeAccountDao.class.getAnnotation(Component.class); + + assertNotNull(component); + } + + @Test + @Order(7) + @DisplayName("AccountService is configured as @Service") + void accountServiceIsConfiguredAsService() { + Service service = AccountService.class.getAnnotation(Service.class); + + assertNotNull(service); + } + + @Test + @Order(8) + @DisplayName("AccountService bean name is not specified explicitly") + void accountServiceBeanNameIsNotSpecifiedExplicitly() { + Service service = AccountService.class.getAnnotation(Service.class); + + assertThat(service.value()).isEqualTo(""); + } + + @Test + @Order(9) + @DisplayName("AccountService doesn't use @Autowired") + void accountServiceDoesNotUseAutowired() throws NoSuchMethodException { + Annotation[] annotations = AccountService.class.getConstructor(AccountDao.class).getDeclaredAnnotations(); + + assertThat(annotations.length).isEqualTo(0); + } + + private Method findTestDataGeneratorBeanMethod(Method[] methods) { + for (Method method : methods) { + if (method.getReturnType().equals(TestDataGenerator.class) + && method.getDeclaredAnnotation(Bean.class) != null) { + return method; + } + } + return null; + } +} diff --git a/3-0-spring-framework/3-0-0-hello-spring-framework/src/test/java/com/bobocode/ApplicationContextTest.java b/3-0-spring-framework/3-0-0-hello-spring-framework/src/test/java/com/bobocode/ApplicationContextTest.java new file mode 100644 index 0000000..6955a14 --- /dev/null +++ b/3-0-spring-framework/3-0-0-hello-spring-framework/src/test/java/com/bobocode/ApplicationContextTest.java @@ -0,0 +1,97 @@ +package com.bobocode; + +import com.bobocode.dao.AccountDao; +import com.bobocode.dao.FakeAccountDao; +import com.bobocode.service.AccountService; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@SpringJUnitConfig +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class ApplicationContextTest { + @Configuration + @ComponentScan(basePackages = "com.bobocode") + static class TestConfig { + } + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private AccountService accountService; + + @Autowired + private AccountDao accountDao; + + @Test + @Order(1) + @DisplayName("DataGenerator has only one bean") + void dataGeneratorHasOnlyOneBean() { + Map testDataGeneratorMap = applicationContext.getBeansOfType(TestDataGenerator.class); + + assertThat(testDataGeneratorMap.size()).isEqualTo(1); + } + + @Test + @Order(2) + @DisplayName("DataGenerator bean has proper name") + void testDataGeneratorBeanName() { + Map dataGeneratorBeanMap = applicationContext.getBeansOfType(TestDataGenerator.class); + + assertThat(dataGeneratorBeanMap.keySet()).contains("dataGenerator"); + } + + @Test + @Order(3) + @DisplayName("AccountDao has only one bean") + void accountDaoHasOnlyOneBean() { + Map accountDaoBeanMap = applicationContext.getBeansOfType(AccountDao.class); + + assertThat(accountDaoBeanMap.size()).isEqualTo(1); + } + + @Test + @Order(4) + @DisplayName("AccountDao bean has proper name") + void accountDaoBeanName() { + Map accountDaoBeanMap = applicationContext.getBeansOfType(AccountDao.class); + + assertThat(accountDaoBeanMap.keySet()).contains("accountDao"); + } + + @Test + @Order(5) + @DisplayName("AccountDao constructor is marked with @Autowired") + void accountDaoConstructorIsMarkedWithAutowired() throws NoSuchMethodException { + Autowired autowired = FakeAccountDao.class.getConstructor(TestDataGenerator.class).getAnnotation(Autowired.class); + + assertNotNull(autowired); + } + + @Test + @Order(6) + @DisplayName("AccountService has only one bean") + void accountServiceHasOnlyOneBean() { + Map accountServiceMap = applicationContext.getBeansOfType(AccountService.class); + + assertThat(accountServiceMap.size()).isEqualTo(1); + } + + @Test + @Order(7) + @DisplayName("AccountService has proper name") + void accountServiceBeanName() { + Map accountServiceMap = applicationContext.getBeansOfType(AccountService.class); + + assertThat(accountServiceMap.keySet()).contains("accountService"); + } +} diff --git a/3-0-spring-framework/pom.xml b/3-0-spring-framework/pom.xml index 209ddd8..f018246 100644 --- a/3-0-spring-framework/pom.xml +++ b/3-0-spring-framework/pom.xml @@ -13,6 +13,7 @@ pom + 3-0-0-hello-spring-framework 3-0-1-hello-spring-mvc 3-1-1-dispatcher-servlet-initializer diff --git a/java-web-course-util/pom.xml b/java-web-course-util/pom.xml index eb5e5a8..1990107 100644 --- a/java-web-course-util/pom.xml +++ b/java-web-course-util/pom.xml @@ -13,6 +13,8 @@ java-web-course-util + spring-framework-exercises-model + spring-framework-exercises-util java-web-util diff --git a/java-web-course-util/spring-framework-exercises-model/pom.xml b/java-web-course-util/spring-framework-exercises-model/pom.xml new file mode 100644 index 0000000..9edb14f --- /dev/null +++ b/java-web-course-util/spring-framework-exercises-model/pom.xml @@ -0,0 +1,14 @@ + + + + java-web-course-util + com.bobocode + 1.0-SNAPSHOT + + 4.0.0 + + spring-framework-exercises-model + + \ No newline at end of file diff --git a/java-web-course-util/spring-framework-exercises-model/src/main/java/com/bobocode/model/Account.java b/java-web-course-util/spring-framework-exercises-model/src/main/java/com/bobocode/model/Account.java new file mode 100644 index 0000000..1a0bb14 --- /dev/null +++ b/java-web-course-util/spring-framework-exercises-model/src/main/java/com/bobocode/model/Account.java @@ -0,0 +1,23 @@ +package com.bobocode.model; + +import lombok.*; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +@NoArgsConstructor +@Getter +@Setter +@ToString +@EqualsAndHashCode(of = "email") +public class Account { + private Long id; + private String firstName; + private String lastName; + private String email; + private LocalDate birthday; + private Gender gender; + private LocalDateTime creationTime; + private BigDecimal balance = BigDecimal.ZERO; +} diff --git a/java-web-course-util/spring-framework-exercises-model/src/main/java/com/bobocode/model/Gender.java b/java-web-course-util/spring-framework-exercises-model/src/main/java/com/bobocode/model/Gender.java new file mode 100644 index 0000000..8a5b149 --- /dev/null +++ b/java-web-course-util/spring-framework-exercises-model/src/main/java/com/bobocode/model/Gender.java @@ -0,0 +1,6 @@ +package com.bobocode.model; + +public enum Gender { + MALE, + FEMALE +} diff --git a/java-web-course-util/spring-framework-exercises-util/pom.xml b/java-web-course-util/spring-framework-exercises-util/pom.xml new file mode 100644 index 0000000..ee655e5 --- /dev/null +++ b/java-web-course-util/spring-framework-exercises-util/pom.xml @@ -0,0 +1,27 @@ + + + + java-web-course-util + com.bobocode + 1.0-SNAPSHOT + + 4.0.0 + + spring-framework-exercises-util + + + + + com.bobocode + spring-framework-exercises-model + 1.0-SNAPSHOT + + + io.codearte.jfairy + jfairy + 0.5.7 + + + \ No newline at end of file diff --git a/java-web-course-util/spring-framework-exercises-util/src/main/java/com/bobocode/TestDataGenerator.java b/java-web-course-util/spring-framework-exercises-util/src/main/java/com/bobocode/TestDataGenerator.java new file mode 100644 index 0000000..50cea04 --- /dev/null +++ b/java-web-course-util/spring-framework-exercises-util/src/main/java/com/bobocode/TestDataGenerator.java @@ -0,0 +1,33 @@ +package com.bobocode; + +import com.bobocode.model.Account; +import com.bobocode.model.Gender; +import io.codearte.jfairy.Fairy; +import io.codearte.jfairy.producer.person.Person; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Random; + +public class TestDataGenerator { + public Account generateAccount() { + Fairy fairy = Fairy.create(); + Person person = fairy.person(); + Random random = new Random(); + + Account fakeAccount = new Account(); + fakeAccount.setFirstName(person.getFirstName()); + fakeAccount.setLastName(person.getLastName()); + fakeAccount.setEmail(person.getEmail()); + fakeAccount.setBirthday(LocalDate.of( + person.getDateOfBirth().getYear(), + person.getDateOfBirth().getMonthOfYear(), + person.getDateOfBirth().getDayOfMonth())); + fakeAccount.setGender(Gender.valueOf(person.getSex().name())); + fakeAccount.setBalance(BigDecimal.valueOf(random.nextInt(200_000))); + fakeAccount.setCreationTime(LocalDateTime.now()); + + return fakeAccount; + } +}