├── .gitignore ├── .travis.yml ├── README.md ├── img ├── aspectj-source-weaving-logo.svg ├── aspectj-source-weaving.svg └── aspectj-src-weaving-logo.svg ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── basaki │ │ ├── Application.java │ │ ├── annotation │ │ └── CustomAnnotation.java │ │ ├── aspect │ │ └── CustomAnnotationAspect.java │ │ ├── config │ │ ├── DataConfiguration.java │ │ ├── SpringConfiguration.java │ │ └── SwaggerConfiguration.java │ │ ├── controller │ │ ├── BookController.java │ │ └── CustomErrorController.java │ │ ├── data │ │ ├── entity │ │ │ └── Book.java │ │ └── repository │ │ │ └── BookRepository.java │ │ ├── error │ │ ├── ErrorInfo.java │ │ ├── ExceptionProcessor.java │ │ └── exception │ │ │ ├── DataNotFoundException.java │ │ │ └── DatabaseException.java │ │ ├── model │ │ └── BookRequest.java │ │ └── service │ │ └── BookService.java └── resources │ ├── config │ └── application.yml │ └── db │ └── create-db.sql └── test └── java └── com └── basaki ├── config └── SwaggerConfigurationFunctionalTests.java ├── controller ├── BookControllerFunctionalTests.java ├── BookControllerTest.java └── CustomErrorControllerTest.java └── error └── ExceptionProcessorTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | *.iml 7 | 8 | ## Directory-based project format: 9 | .idea 10 | .idea/*.xml 11 | 12 | # if you remove the above rule, at least ignore the following: 13 | 14 | # User-specific stuff: 15 | .idea/workspace.xml 16 | .idea/tasks.xml 17 | .idea/dictionaries 18 | .idea/vcs.xml 19 | .idea/jsLibraryMappings.xml 20 | 21 | # Sensitive or high-churn files: 22 | .idea/dataSources.ids 23 | .idea/dataSources.xml 24 | .idea/dataSources.local.xml 25 | .idea/sqlDataSources.xml 26 | .idea/dynamic.xml 27 | .idea/uiDesigner.xml 28 | 29 | # Gradle: 30 | .idea/gradle.xml 31 | .idea/libraries 32 | 33 | # Mongo Explorer plugin: 34 | .idea/mongoSettings.xml 35 | 36 | ## File-based project format: 37 | *.iws 38 | 39 | ## Plugin-specific files: 40 | 41 | # IntelliJ 42 | /out/ 43 | 44 | # mpeltonen/sbt-idea plugin 45 | .idea_modules/ 46 | 47 | # JIRA plugin 48 | atlassian-ide-plugin.xml 49 | 50 | # Crashlytics plugin (for Android Studio and IntelliJ) 51 | com_crashlytics_export_strings.xml 52 | crashlytics.properties 53 | crashlytics-build.properties 54 | fabric.properties 55 | ### OSX template 56 | *.DS_Store 57 | .AppleDouble 58 | .LSOverride 59 | 60 | # Icon must end with two \r 61 | Icon 62 | 63 | # Thumbnails 64 | ._* 65 | 66 | # Files that might appear in the root of a volume 67 | .DocumentRevisions-V100 68 | .fseventsd 69 | .Spotlight-V100 70 | .TemporaryItems 71 | .Trashes 72 | .VolumeIcon.icns 73 | .com.apple.timemachine.donotpresent 74 | 75 | # Directories potentially created on remote AFP share 76 | .AppleDB 77 | .AppleDesktop 78 | Network Trash Folder 79 | Temporary Items 80 | .apdisk 81 | ### EiffelStudio template 82 | # The compilation directory 83 | EIFGENs 84 | ### Xcode template 85 | # Xcode 86 | # 87 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 88 | 89 | ## Build generated 90 | build/ 91 | DerivedData/ 92 | 93 | ## Various settings 94 | *.pbxuser 95 | !default.pbxuser 96 | *.mode1v3 97 | !default.mode1v3 98 | *.mode2v3 99 | !default.mode2v3 100 | *.perspectivev3 101 | !default.perspectivev3 102 | xcuserdata/ 103 | 104 | ## Other 105 | *.moved-aside 106 | *.xccheckout 107 | *.xcscmblueprint 108 | ### Java template 109 | *.class 110 | 111 | # Mobile Tools for Java (J2ME) 112 | .mtj.tmp/ 113 | 114 | # Package Files # 115 | *.war 116 | *.ear 117 | 118 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 119 | hs_err_pid* 120 | 121 | ## Eclipse 122 | .project 123 | .classpath 124 | 125 | ## Directory-based project format: 126 | .settings/ 127 | # if you remove the above rule, at least ignore the following: 128 | 129 | ### Maven template 130 | target 131 | pom.xml.tag 132 | pom.xml.releaseBackup 133 | pom.xml.versionsBackup 134 | pom.xml.next 135 | release.properties 136 | dependency-reduced-pom.xml 137 | 138 | /consul/macos/data/raft/ 139 | /consul/macos/data/services/ 140 | /consul/macos/data/checks/ 141 | /consul/macos/data/node-id 142 | /consul/macos/data/config/ 143 | /consul/macos/data/serf/ 144 | /consul/macos/data/checkpoint-signature 145 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | before_script: 5 | - mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V 6 | script: 7 | - mvn clean install sonar:sonar -Dsonar.host.url=https://sonarcloud.io -Dsonar.organization=indrabasak-github 8 | -Dsonar.login=$SONAR_TOKEN 9 | cache: 10 | directories: "– $HOME/.m2 – $HOME/.sonar/cache" 11 | notifications: 12 | email: 13 | - indra.basak1@gmail.com 14 | on_success: change 15 | on_failure: always 16 | use_notice: true 17 | env: 18 | global: 19 | secure: SBbcX10RrVNsTfq2fgD7+wRfRfCw/LEJMe2OdjXhYw5OuWd6zalAQNAr44iDGOvx50MmzpfAqG99QGb5YvTYMun/8yHGa2i1Dyq0H4WnzNSja1vT1qoJs37xD1K6USLxHmnxpj01gJI0Gl6djNn6I1XW1FDvUWU+TC0jxcg1rQnTco7UYPk0sZedNpU5yo0gt4w+3BKAR7WTEVLe+850D2yBZGuKYebueU9xk1ycwTpZyCSbErdiyQWfjyBx8lcvf2jooLtJHJRBOPJLuFdNdo3RgPFNuXuiCEeNrhj6gkCDYlkXoNbS0Zx0agCsn0i73DsmDyDDwjd6Zjci/pL6GnUeTgZ3i4oL7GYb8sR74tEjlhwcwTme6eM9Mve7g1f5aDnR2/DvrASHqGBR0jMVKfCgkD/UTJVdE+M8Dqxv2m3ZW1kGERSqsw7Pngy7fdtuwHZ+tZlX/sV4fsNu2KIkMFX/4qb2eq4F52DlS8WsHSFuCLZoOIlt+xqv70phN8Mt4hBasmR+PWy0lelM4ff63ATWPTyFpeiI7l+Hcf5rguuqN0R5wTSmuIuRw1p68/ezNxMii/wtpkidoEgYCTwf0OwREqDs5E04e2aAyib0pa+1ct9Ea2h2Z6QOWPlWMb9r4f5WXD9Fo19cnvuRGHyHsnDWEufEWy882mx0mX+SeKA= 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status][travis-badge]][travis-badge-url] 2 | [![Quality Gate][sonarqube-badge]][sonarqube-badge-url] 3 | [![Technical debt ratio][technical-debt-ratio-badge]][technical-debt-ratio-badge-url] 4 | [![Coverage][coverage-badge]][coverage-badge-url] 5 | 6 | ![](./img/aspectj-source-weaving-logo.svg) 7 | 8 | Spring Boot Source Weaving (Compile time) Example with AspectJ 9 | =============================================================== 10 | This is an example of Spring Boot source weaving (compile time) with AspectJ. 11 | 12 | ### Source Weaving 13 | AspectJ source weaving is compile-time weaving when all source code is available 14 | including annotation class, aspect class, and target class. 15 | 16 | The AspectJ compiler (`ajc`) processes the source code and generates woven 17 | byte code. All the source code should be present together at the compile time. 18 | 19 | ![](./img/aspectj-source-weaving.svg) 20 | 21 | ### When do you need source weaving? 22 | 1. Due to the proxy-based nature of Spring’s AOP framework, calls within the 23 | target object are by definition not intercepted. 24 | 25 | 1. For JDK proxies, only public interface method calls on the proxy can be 26 | intercepted. With CGLIB, public and protected method calls on the proxy will 27 | be intercepted, and even package-visible methods if necessary. 28 | 29 | You can find more [here](https://docs.spring.io/spring/docs/4.3.x/spring-framework-reference/html/aop.html#Supported%20Pointcut%20Designators). 30 | 31 | In other words, 32 | 33 | 1. Any call to a **private method** will not be intercepted. Please refer to 34 | the second point mentioned above. 35 | 36 | 1. Any call to method **methodB** of class **ClassX*** from **methodA** of 37 | class **ClassX** will not be intercepted since they belong to the same target 38 | object. Please refer to the first point above. 39 | 40 | AspectJ source weaving will help you get past the above limitations posed by 41 | Spring AOP. 42 | 43 | ### Project Description 44 | 1. A `CustomAnnotation` annotation to intercept any method. 45 | 46 | ```java 47 | @Target({ElementType.METHOD}) 48 | @Retention(RetentionPolicy.RUNTIME) 49 | public @interface CustomAnnotation { 50 | 51 | String description() default ""; 52 | } 53 | ``` 54 | 55 | 1. A `CustomAnnotationAspect` aspect to intercept any method marked with 56 | `@CustomAnnotation`. It prints out the name of the intercepted class and method. 57 | 58 | ```java 59 | @Component 60 | @Aspect 61 | @Slf4j 62 | public class CustomAnnotationAspect { 63 | 64 | @Before("@annotation(anno) && execution(* *(..))") 65 | public void inspectMethod(JoinPoint jp, CustomAnnotation anno) { 66 | log.info( 67 | "Entering CustomAnnotationAspect.inspectMethod() in class " 68 | + jp.getSignature().getDeclaringTypeName() 69 | + " - method: " + jp.getSignature().getName() 70 | + " description: " + anno.description()); 71 | } 72 | } 73 | ``` 74 | 75 | 1. The `BookService` class is the example where the `@CustomAnnotation` is used. 76 | The **privat**e method `validateRequest` is called from `create` method. The 77 | `create` method is annotated with Spring's `@Transactional` annotation. 78 | 79 | ```java 80 | @Service 81 | @Slf4j 82 | public class BookService { 83 | 84 | private BookRepository repository; 85 | 86 | @Autowired 87 | public BookService(BookRepository repository) { 88 | this.repository = repository; 89 | } 90 | 91 | @Transactional 92 | public Book create(BookRequest request) { 93 | Book entity = validateRequest(request); 94 | return repository.save(entity); 95 | } 96 | 97 | public Book read(UUID id) { 98 | return repository.getOne(id); 99 | } 100 | 101 | @CustomAnnotation(description = "Validates book request.") 102 | private Book validateRequest(BookRequest request) { 103 | log.info("Validating book request!"); 104 | 105 | Assert.notNull(request, "Book request cannot be empty!"); 106 | Assert.notNull(request.getTitle(), "Book title cannot be missing!"); 107 | Assert.notNull(request.getAuthor(), "Book author cannot be missing!"); 108 | 109 | Book entity = new Book(); 110 | entity.setTitle(request.getTitle()); 111 | entity.setAuthor(request.getAuthor()); 112 | 113 | return entity; 114 | } 115 | } 116 | ``` 117 | 118 | ### Dependency Requirements 119 | 120 | #### AspectJ Runtime Library 121 | Annotation such as `@Aspect`, `@Pointcut`, and `@Before` are in `aspectjrt.jar`. 122 | The `aspectjrt.jar` and must be in the classpath regardless of whether 123 | the aspects in the code are compiled with `ajc` or `javac`. 124 | 125 | ```xml 126 | 127 | org.aspectj 128 | aspectjrt 129 | 1.8.13 130 | 131 | ``` 132 | 133 | #### AspectJ Weaving Library 134 | The `aspectjweaver.jar` contains the AspectJ wevaing classes. The weaver is 135 | responsible for mapping crosscutting elements to Java constructs. 136 | 137 | ```xml 138 | 139 | org.aspectj 140 | aspectjweaver 141 | 1.8.13 142 | 143 | ``` 144 | 145 | #### AspectJ Maven Plugin 146 | The `aspectj-maven-plugin` plugin is used for weaving AspectJ aspects into 147 | the classes using `ajc` (AspectJ compiler) during compile time. 148 | 149 | ```xml 150 | 151 | org.codehaus.mojo 152 | aspectj-maven-plugin 153 | 1.11 154 | 155 | true 156 | 1.8 157 | 1.8 158 | 1.8 159 | ignore 160 | true 161 | 162 | 163 | ${project.build.directory}/classes 164 | 165 | 166 | 167 | 168 | 169 | compile 170 | test-compile 171 | 172 | 173 | 174 | 175 | 176 | org.aspectj 177 | aspectjrt 178 | 1.8.13 179 | 180 | 181 | org.aspectj 182 | aspectjtools 183 | 1.8.13 184 | 185 | 186 | 187 | ``` 188 | 189 | #### Lombok Maven Plugin (Optional) 190 | The `lombok-maven-plugin` is required only if any of the classes uses Lombok 191 | annotations. The Lombok annotated classes are delomboked before compiled with 192 | the `ajc`. 193 | 194 | ```xml 195 | 196 | org.projectlombok 197 | lombok-maven-plugin 198 | 1.16.20.0 199 | 200 | 201 | generate-sources 202 | 203 | delombok 204 | 205 | 206 | 207 | 208 | false 209 | src/main/java 210 | UTF-8 211 | 212 | 213 | ``` 214 | 215 | ### Build 216 | To build the JAR, execute the following command from the parent directory: 217 | 218 | ``` 219 | mvn clean install 220 | ``` 221 | 222 | ### Run 223 | Run the executable jar from the command to start the application, 224 | 225 | ``` 226 | java -jar spring-source-weaving-example-1.0.0.jar 227 | ``` 228 | 229 | ### Usage 230 | Once the application starts up at port `8080`, you can access the swagger UI at 231 | `http://localhost:8080/swagger-ui.html`. From the UI, you can create and retrieve 232 | book entities. 233 | 234 | Once you create a book entity, you should notice the following message on the 235 | terminal: 236 | 237 | ``` 238 | 2018-02-08 09:46:55.429 INFO 29924 --- [nio-8080-exec-1] c.basaki.aspect.CustomAnnotationAspect : Entering CustomAnnotationAspect.inspectMethod() in class com.basaki.service.BookService - method: validateRequest description: Validates book request. 239 | 2018-02-08 09:46:55.429 INFO 29924 --- [nio-8080-exec-1] com.basaki.service.BookService : Validating book request! 240 | ``` 241 | 242 | 243 | [travis-badge]: https://travis-ci.org/indrabasak/spring-source-weaving-example.svg?branch=master 244 | [travis-badge-url]: https://travis-ci.org/indrabasak/spring-source-weaving-example/ 245 | 246 | [sonarqube-badge]: https://sonarcloud.io/api/project_badges/measure?project=com.basaki%3Aspring-source-weaving-example&metric=alert_status 247 | [sonarqube-badge-url]: https://sonarcloud.io/dashboard/index/com.basaki:spring-source-weaving-example 248 | 249 | [technical-debt-ratio-badge]: https://sonarcloud.io/api/project_badges/measure?project=com.basaki%3Aspring-source-weaving-example&metric=sqale_index 250 | [technical-debt-ratio-badge-url]: https://sonarcloud.io/dashboard/index/com.basaki:spring-source-weaving-example 251 | 252 | [coverage-badge]: https://sonarcloud.io/api/project_badges/measure?project=com.basaki%3Aspring-source-weaving-example&metric=coverage 253 | [coverage-badge-url]: https://sonarcloud.io/dashboard/index/com.basaki:spring-source-weaving-example 254 | -------------------------------------------------------------------------------- /img/aspectj-source-weaving.svg: -------------------------------------------------------------------------------- 1 | 2 |
Java Source files (.java)
Java Source files (.java)
AspectJ Source files (.java)
AspectJ Source files (.java)
+
+
ajc (AspectJ Compiler)
[Not supported by viewer]
Woven Class files (.class)
Woven Class files (.class)
-------------------------------------------------------------------------------- /img/aspectj-src-weaving-logo.svg: -------------------------------------------------------------------------------- 1 | 2 |
aspectJ 
Source Weaving
[Not supported by viewer]
-------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | com.basaki 7 | spring-source-weaving-example 8 | 1.0.0 9 | Spring Boot Source Weaving Example with AspectJ 10 | 11 | 12 | 1.8.13 13 | 2.8.8 14 | 1.8 15 | 4.12 16 | 1.16.20 17 | 2.13.0 18 | 2.0.0.RC1 19 | 2.8.0 20 | 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-dependencies 27 | ${spring.boot.version} 28 | pom 29 | import 30 | 31 | 32 | 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-web 38 | 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-data-jpa 44 | 45 | 46 | org.apache.logging.log4j 47 | log4j-slf4j-impl 48 | 49 | 50 | 51 | 52 | org.hsqldb 53 | hsqldb 54 | 2.3.4 55 | 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-starter-actuator 60 | 61 | 62 | 63 | 64 | org.aspectj 65 | aspectjrt 66 | ${aspectj.version} 67 | 68 | 69 | org.aspectj 70 | aspectjweaver 71 | ${aspectj.version} 72 | 73 | 74 | 75 | org.projectlombok 76 | lombok 77 | ${lombok.version} 78 | provided 79 | 80 | 81 | 82 | io.springfox 83 | springfox-swagger2 84 | ${swagger.version} 85 | 86 | 87 | 88 | io.springfox 89 | springfox-swagger-ui 90 | ${swagger.version} 91 | 92 | 93 | 94 | 95 | org.springframework.boot 96 | spring-boot-starter-test 97 | test 98 | 99 | 100 | 101 | junit 102 | junit 103 | ${junit.version} 104 | test 105 | 106 | 107 | 108 | io.rest-assured 109 | rest-assured 110 | 3.0.5 111 | test 112 | 113 | 114 | io.rest-assured 115 | spring-mock-mvc 116 | 3.0.0 117 | test 118 | 119 | 120 | org.hamcrest 121 | hamcrest-core 122 | 1.3 123 | test 124 | 125 | 126 | me.prettyprint 127 | hector-core 128 | 1.0-5 129 | test 130 | 131 | 132 | 133 | org.mockito 134 | mockito-core 135 | ${mockito.version} 136 | test 137 | 138 | 139 | 140 | 141 | 142 | sonatype-oss-snapshots 143 | Sonatype OSS Snapshots Repository 144 | https://oss.sonatype.org/content/repositories/snapshots 145 | 146 | 147 | spring-snapshots 148 | Spring Snapshots 149 | https://repo.spring.io/libs-snapshot 150 | 151 | true 152 | 153 | 154 | 155 | spring-milestones 156 | Spring Milestones 157 | https://repo.spring.io/libs-milestone 158 | 159 | false 160 | 161 | 162 | 163 | 164 | 165 | 166 | repository.spring.release 167 | Spring GA Repository 168 | https://repo.spring.io/plugins-release/ 169 | 170 | true 171 | 172 | 173 | true 174 | 175 | 176 | 177 | spring-snapshots 178 | http://repo.spring.io/snapshot 179 | 180 | true 181 | 182 | 183 | true 184 | 185 | 186 | 187 | spring-milestones 188 | Spring Milestones 189 | http://repo.spring.io/milestone 190 | 191 | true 192 | 193 | 194 | true 195 | 196 | 197 | 198 | 199 | 200 | 201 | ${project.build.directory}/generated-sources/delombok 202 | 203 | 204 | 205 | 206 | maven-compiler-plugin 207 | 3.7.0 208 | 209 | ${java.version} 210 | ${java.version} 211 | 212 | 213 | 214 | 215 | 216 | org.projectlombok 217 | lombok-maven-plugin 218 | 1.16.20.0 219 | 220 | 221 | generate-sources 222 | 223 | delombok 224 | 225 | 226 | 227 | 228 | false 229 | src/main/java 230 | UTF-8 231 | 232 | 233 | 234 | 235 | org.codehaus.mojo 236 | aspectj-maven-plugin 237 | 1.11 238 | 239 | true 240 | ${java.version} 241 | ${java.version} 242 | ${java.version} 243 | ignore 244 | true 245 | 246 | 247 | ${project.build.directory}/classes 248 | 249 | 250 | 251 | 252 | 253 | 254 | compile 255 | test-compile 256 | 257 | 258 | 259 | 260 | 261 | org.aspectj 262 | aspectjrt 263 | ${aspectj.version} 264 | 265 | 266 | org.aspectj 267 | aspectjtools 268 | ${aspectj.version} 269 | 270 | 271 | 272 | 273 | 274 | org.springframework.boot 275 | spring-boot-maven-plugin 276 | ${spring.boot.version} 277 | 278 | com.basaki.Application 279 | 280 | 281 | 282 | 283 | repackage 284 | 285 | 286 | 287 | 288 | 289 | org.jacoco 290 | jacoco-maven-plugin 291 | 0.7.9 292 | 293 | 294 | default-prepare-agent 295 | 296 | prepare-agent 297 | 298 | 299 | ${sonar.jacoco.utReportPath} 300 | surefireArgLine 301 | 302 | 303 | 304 | default-report 305 | prepare-package 306 | 307 | report 308 | 309 | 310 | 311 | pre-unit-test 312 | 313 | prepare-agent 314 | 315 | 316 | 317 | post-unit-test 318 | test 319 | 320 | report 321 | 322 | 323 | ${sonar.jacoco.utReportPath} 324 | ${project.reporting.outputDirectory}/jacoco-ut 325 | 326 | 327 | 328 | pre-integration-test 329 | pre-integration-test 330 | 331 | prepare-agent 332 | 333 | 334 | ${sonar.jacoco.reportPaths} 335 | failsafe.argLine 336 | 337 | 338 | 339 | post-integration-test 340 | post-integration-test 341 | 342 | report 343 | 344 | 345 | ${sonar.jacoco.reportPaths} 346 | 347 | 348 | 349 | 350 | 351 | org.apache.maven.plugins 352 | maven-surefire-plugin 353 | 2.20.1 354 | 355 | ${surefireArgLine} 356 | 357 | **/*Test.java 358 | **/*Tests.java 359 | 360 | 361 | 362 | 363 | org.apache.maven.plugins 364 | maven-failsafe-plugin 365 | 2.18.1 366 | 367 | 368 | **/*FunctionalTests.java 369 | **/*IntegrationTests.java 370 | **/*PerformanceTests.java 371 | 372 | 373 | 374 | 375 | 376 | integration-test 377 | verify 378 | 379 | 380 | 381 | 382 | 383 | org.sonarsource.scanner.maven 384 | sonar-maven-plugin 385 | 3.3.0.603 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | org.apache.maven.plugins 394 | maven-pmd-plugin 395 | 3.1 396 | 397 | ${java.version} 398 | 399 | 400 | 401 | org.apache.maven.plugins 402 | maven-javadoc-plugin 403 | 2.9.1 404 | 405 | 406 | org.apache.maven.plugins 407 | maven-surefire-report-plugin 408 | 2.17 409 | 410 | 411 | 412 | -------------------------------------------------------------------------------- /src/main/java/com/basaki/Application.java: -------------------------------------------------------------------------------- 1 | package com.basaki; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.ComponentScan; 6 | 7 | /** 8 | * {@code BookApplication} represents the entry point for the Spring 9 | * boot application example. 10 | *

11 | * 12 | * @author Indra Basak 13 | * @since 12/27/17 14 | */ 15 | @SpringBootApplication 16 | @ComponentScan(basePackages = {"com.basaki"}) 17 | public class Application { 18 | 19 | public static void main(String[] args) { 20 | SpringApplication.run(Application.class, args); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/basaki/annotation/CustomAnnotation.java: -------------------------------------------------------------------------------- 1 | package com.basaki.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 | * {@code CustomAnnotation} is annotation for marking a method. 10 | *

11 | * Given a method like this: 12 | *


13 |  *     {@literal @}CustomAnnotation(description = "My description")
14 |  *     public String someMethod(String name) {
15 |  *         return "Hello " + name;
16 |  *     }
17 |  * 
18 | *

19 | * 20 | * @author Indra Basak 21 | * @since 02/07/18 22 | */ 23 | @Target({ElementType.METHOD}) 24 | @Retention(RetentionPolicy.RUNTIME) 25 | public @interface CustomAnnotation { 26 | 27 | String description() default ""; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/basaki/aspect/CustomAnnotationAspect.java: -------------------------------------------------------------------------------- 1 | package com.basaki.aspect; 2 | 3 | import com.basaki.annotation.CustomAnnotation; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.aspectj.lang.JoinPoint; 6 | import org.aspectj.lang.annotation.Aspect; 7 | import org.aspectj.lang.annotation.Before; 8 | import org.springframework.stereotype.Component; 9 | 10 | /** 11 | * {@code CustomAnnotationAspect} intercepts any private method execution if a 12 | * method is tagged with {@code com.basaki.annotation.CustomAnnotation} 13 | * annotation. 14 | *

15 | * 16 | * @author Indra Basak 17 | * @since 02/07/18 18 | */ 19 | @Component 20 | @Aspect 21 | @Slf4j 22 | public class CustomAnnotationAspect { 23 | 24 | @Before("@annotation(anno) && execution(* *(..))") 25 | public void inspectMethod(JoinPoint jp, CustomAnnotation anno) { 26 | log.info( 27 | "Entering CustomAnnotationAspect.inspectMethod() in class " 28 | + jp.getSignature().getDeclaringTypeName() 29 | + " - method: " + jp.getSignature().getName() 30 | + " description: " + anno.description()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/basaki/config/DataConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.basaki.config; 2 | 3 | import javax.sql.DataSource; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 7 | import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; 8 | import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; 9 | import org.springframework.transaction.annotation.EnableTransactionManagement; 10 | 11 | /** 12 | * {@code DataConfiguration} configures an embedded database. 13 | *

14 | * 15 | * @author Indra Basak 16 | * @since 11/23/17 17 | */ 18 | @Configuration 19 | @EnableJpaRepositories(basePackages = {"com.basaki.data.repository"}) 20 | @EnableTransactionManagement 21 | public class DataConfiguration { 22 | 23 | @Bean 24 | public DataSource dataSource() { 25 | EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); 26 | return builder 27 | .setType(EmbeddedDatabaseType.HSQL) 28 | .addScript("db/create-db.sql") 29 | .build(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/basaki/config/SpringConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.basaki.config; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import java.util.Arrays; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Primary; 8 | import org.springframework.http.MediaType; 9 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 10 | import org.springframework.web.filter.CommonsRequestLoggingFilter; 11 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 12 | 13 | /** 14 | * {@code SpringConfiguration} creates bean for logging request. 15 | *

16 | * 17 | * @author Indra Basak 18 | * @since 12/27/17 19 | */ 20 | @Configuration 21 | public class SpringConfiguration implements WebMvcConfigurer { 22 | 23 | @Primary 24 | @Bean(name = "customObjectMapper") 25 | public ObjectMapper createObjectMapper() { 26 | return new ObjectMapper(); 27 | } 28 | 29 | @Bean 30 | public MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter() { 31 | MappingJackson2HttpMessageConverter standardConverter = 32 | new MappingJackson2HttpMessageConverter(); 33 | standardConverter.setPrefixJson(false); 34 | standardConverter.setSupportedMediaTypes(Arrays.asList( 35 | MediaType.APPLICATION_JSON, 36 | MediaType.TEXT_PLAIN)); 37 | standardConverter.setObjectMapper(createObjectMapper()); 38 | return standardConverter; 39 | } 40 | 41 | @Bean 42 | public CommonsRequestLoggingFilter logFilter() { 43 | CommonsRequestLoggingFilter filter 44 | = new CommonsRequestLoggingFilter(); 45 | filter.setIncludeQueryString(true); 46 | filter.setIncludePayload(true); 47 | filter.setMaxPayloadLength(10000); 48 | filter.setIncludeHeaders(true); 49 | filter.setAfterMessagePrefix("REQUEST DATA : "); 50 | return filter; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/basaki/config/SwaggerConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.basaki.config; 2 | 3 | import com.google.common.base.Predicate; 4 | import java.util.ArrayList; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import springfox.documentation.RequestHandler; 8 | import springfox.documentation.builders.PathSelectors; 9 | import springfox.documentation.service.ApiInfo; 10 | import springfox.documentation.service.Contact; 11 | import springfox.documentation.service.VendorExtension; 12 | import springfox.documentation.spi.DocumentationType; 13 | import springfox.documentation.spring.web.plugins.Docket; 14 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 15 | 16 | /** 17 | * {@code SwaggerConfiguration} configures Swagger UI. 18 | *

19 | * 20 | * @author Indra Basak 21 | * @since 12/27/17 22 | */ 23 | @Configuration 24 | @EnableSwagger2 25 | @SuppressWarnings({"squid:CallToDeprecatedMethod"}) 26 | public class SwaggerConfiguration { 27 | 28 | /** 29 | * Creates the Swagger Docket (configuration) bean. 30 | * 31 | * @return docket bean 32 | */ 33 | @Bean 34 | public Docket api() { 35 | return new Docket(DocumentationType.SWAGGER_2) 36 | .groupName("book") 37 | .select() 38 | .apis(exactPackage("com.basaki.controller")) 39 | .paths(PathSelectors.any()) 40 | .build() 41 | .apiInfo(apiInfo("Book Sevice API", 42 | "An example of using TLS with Spring Boot")); 43 | } 44 | 45 | /** 46 | * Creates an object containing API information including version name, 47 | * license, etc. 48 | * 49 | * @param title API title 50 | * @param description API description 51 | * @return API information 52 | */ 53 | private ApiInfo apiInfo(String title, String description) { 54 | Contact contact = new Contact("Indra Basak", "", 55 | "indra@basak.com"); 56 | return new ApiInfo(title, description, "1.0.0", 57 | "terms of service url", 58 | contact, "license", "license url", 59 | new ArrayList()); 60 | } 61 | 62 | private static Predicate exactPackage(final String pkg) { 63 | return input -> input.declaringClass().getPackage().getName().equals( 64 | pkg); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/basaki/controller/BookController.java: -------------------------------------------------------------------------------- 1 | package com.basaki.controller; 2 | 3 | import com.basaki.data.entity.Book; 4 | import com.basaki.model.BookRequest; 5 | import com.basaki.service.BookService; 6 | import io.swagger.annotations.Api; 7 | import io.swagger.annotations.ApiOperation; 8 | import java.util.UUID; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.MediaType; 13 | import org.springframework.web.bind.annotation.PathVariable; 14 | import org.springframework.web.bind.annotation.RequestBody; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.RequestMethod; 17 | import org.springframework.web.bind.annotation.ResponseStatus; 18 | import org.springframework.web.bind.annotation.RestController; 19 | 20 | /** 21 | * {@code BookController} exposes book service. 22 | *

23 | * 24 | * @author Indra Basak 25 | * @since 11/20/17 26 | */ 27 | @RestController 28 | @Slf4j 29 | @Api(value = "Book Service", produces = "application/json", tags = {"1"}) 30 | public class BookController { 31 | 32 | private BookService service; 33 | 34 | @Autowired 35 | public BookController(BookService service) { 36 | this.service = service; 37 | } 38 | 39 | @ApiOperation(value = "Creates a book.", response = Book.class) 40 | @RequestMapping(method = RequestMethod.POST, value = "/books") 41 | @ResponseStatus(HttpStatus.CREATED) 42 | public Book create(@RequestBody BookRequest request) { 43 | return service.create(request); 44 | } 45 | 46 | @ApiOperation(value = "Retrieves a book.", notes = "Requires book identifier", 47 | response = Book.class) 48 | @RequestMapping(method = RequestMethod.GET, produces = { 49 | MediaType.APPLICATION_JSON_VALUE}, value = "/books/{id}") 50 | public Book read(@PathVariable("id") UUID id) { 51 | return service.read(id); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/basaki/controller/CustomErrorController.java: -------------------------------------------------------------------------------- 1 | package com.basaki.controller; 2 | 3 | import com.basaki.error.ErrorInfo; 4 | import java.util.Map; 5 | import javax.servlet.http.HttpServletRequest; 6 | import javax.servlet.http.HttpServletResponse; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.boot.web.servlet.error.ErrorAttributes; 11 | import org.springframework.boot.web.servlet.error.ErrorController; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RestController; 14 | import org.springframework.web.context.request.ServletWebRequest; 15 | import org.springframework.web.context.request.WebRequest; 16 | import springfox.documentation.annotations.ApiIgnore; 17 | 18 | /** 19 | * {@code CustomErrorController} used for showing error messages. 20 | *

21 | * 22 | * @author Indra Basak 23 | * @since 12/27/17 24 | */ 25 | @RestController 26 | @ApiIgnore 27 | @Slf4j 28 | @SuppressWarnings({"squid:S1075"}) 29 | public class CustomErrorController implements ErrorController { 30 | 31 | private static final String PATH = "/error"; 32 | 33 | @Value("${debug:true}") 34 | private boolean debug; 35 | 36 | private ErrorAttributes errorAttributes; 37 | 38 | @Autowired 39 | public CustomErrorController(ErrorAttributes errorAttributes) { 40 | this.errorAttributes = errorAttributes; 41 | } 42 | 43 | @RequestMapping(value = PATH) 44 | ErrorInfo error(HttpServletRequest request, HttpServletResponse response) { 45 | ErrorInfo info = new ErrorInfo(); 46 | info.setCode(response.getStatus()); 47 | Map attributes = getErrorAttributes(request, debug); 48 | info.setMessage((String) attributes.get("message")); 49 | log.error((String) attributes.get("error")); 50 | 51 | return info; 52 | } 53 | 54 | @Override 55 | public String getErrorPath() { 56 | return PATH; 57 | } 58 | 59 | private Map getErrorAttributes(HttpServletRequest request, 60 | boolean includeStackTrace) { 61 | WebRequest webRequest = 62 | new ServletWebRequest(request); 63 | return errorAttributes.getErrorAttributes(webRequest, 64 | includeStackTrace); 65 | } 66 | } -------------------------------------------------------------------------------- /src/main/java/com/basaki/data/entity/Book.java: -------------------------------------------------------------------------------- 1 | package com.basaki.data.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import io.swagger.annotations.ApiModel; 5 | import io.swagger.annotations.ApiModelProperty; 6 | import java.io.Serializable; 7 | import java.util.UUID; 8 | import javax.persistence.Column; 9 | import javax.persistence.Entity; 10 | import javax.persistence.GeneratedValue; 11 | import javax.persistence.Id; 12 | import javax.persistence.Table; 13 | import lombok.Data; 14 | import lombok.NoArgsConstructor; 15 | import org.hibernate.annotations.GenericGenerator; 16 | 17 | 18 | /** 19 | * {@code Book} represents a book entity. 20 | *

21 | * 22 | * @author Indra Basak 23 | * @since 11/23/17 24 | */ 25 | @Entity 26 | @Table(name = "book") 27 | @Data 28 | @NoArgsConstructor 29 | @ApiModel(value = "Book") 30 | @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) 31 | public class Book implements Serializable { 32 | 33 | @ApiModelProperty(value = "identity of a book") 34 | @Id 35 | @GeneratedValue(generator = "UUID") 36 | @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator") 37 | @Column(name = "id", columnDefinition = "BINARY(16)") 38 | private UUID id; 39 | 40 | @ApiModelProperty(value = "book title") 41 | @Column(name = "title", nullable = false) 42 | private String title; 43 | 44 | @ApiModelProperty(value = "book author") 45 | @Column(name = "author", nullable = false) 46 | private String author; 47 | } -------------------------------------------------------------------------------- /src/main/java/com/basaki/data/repository/BookRepository.java: -------------------------------------------------------------------------------- 1 | package com.basaki.data.repository; 2 | 3 | import com.basaki.data.entity.Book; 4 | import java.util.UUID; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | /** 9 | * {@code BookRepository} is a JPA book repository. It servers as an example 10 | * for springfox-data-rest. 11 | *

12 | * 13 | * @author Indra Basak 14 | * @since 11/23/17 15 | */ 16 | @Repository 17 | public interface BookRepository extends JpaRepository { 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/basaki/error/ErrorInfo.java: -------------------------------------------------------------------------------- 1 | package com.basaki.error; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | 7 | /** 8 | * {@code ErrorInfo} represents an error response object which is exposed to 9 | * the external client. It is human readable and informative without 10 | * exposing service implementation details, e.g., 11 | * exception type, stack trace, etc. 12 | *

13 | * 14 | * @author Indra Basak 15 | * @since 12/27/16 16 | */ 17 | @NoArgsConstructor 18 | @Getter 19 | @Setter 20 | public class ErrorInfo { 21 | 22 | private String path; 23 | 24 | private int code; 25 | 26 | private String type; 27 | 28 | private String message; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/basaki/error/ExceptionProcessor.java: -------------------------------------------------------------------------------- 1 | package com.basaki.error; 2 | 3 | import com.basaki.error.exception.DataNotFoundException; 4 | import com.basaki.error.exception.DatabaseException; 5 | import javax.servlet.http.HttpServletRequest; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.web.bind.annotation.ControllerAdvice; 9 | import org.springframework.web.bind.annotation.ExceptionHandler; 10 | import org.springframework.web.bind.annotation.ResponseBody; 11 | import org.springframework.web.bind.annotation.ResponseStatus; 12 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder; 13 | 14 | /** 15 | * {@code ExceptionProcessor} processes exceptions at the application level and 16 | * is not restricted to any specific controller. 17 | *

18 | * 19 | * @author Indra Basak 20 | * @since 12/28/17 21 | */ 22 | @ControllerAdvice 23 | @Slf4j 24 | public class ExceptionProcessor { 25 | 26 | /** 27 | * Handles DataNotFoundException exception.It unwraps the root case 28 | * and coverts it into an ErrorInfo object. 29 | * 30 | * @param req HTTP request to extract the URL 31 | * @param ex exception to be processed 32 | * @return ths error information that is sent to the client 33 | */ 34 | @ExceptionHandler(DataNotFoundException.class) 35 | @ResponseStatus(value = HttpStatus.NOT_FOUND) 36 | @ResponseBody 37 | public ErrorInfo handleDataNotFoundException( 38 | HttpServletRequest req, DataNotFoundException ex) { 39 | ErrorInfo info = getErrorInfo(req, HttpStatus.NOT_FOUND); 40 | info.setMessage(ex.getMessage()); 41 | 42 | return info; 43 | } 44 | 45 | /** 46 | * Handles DatabaseException exception.It unwraps the root case 47 | * and coverts it into an ErrorInfo object. 48 | * 49 | * @param req HTTP request to extract the URL 50 | * @param ex exception to be processed 51 | * @return ths error information that is sent to the client 52 | */ 53 | @ExceptionHandler(DatabaseException.class) 54 | @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) 55 | @ResponseBody 56 | public ErrorInfo handleDatabaseException( 57 | HttpServletRequest req, DatabaseException ex) { 58 | ErrorInfo info = getErrorInfo(req, HttpStatus.INTERNAL_SERVER_ERROR); 59 | info.setMessage(ex.getMessage()); 60 | 61 | return info; 62 | } 63 | 64 | private ErrorInfo getErrorInfo(HttpServletRequest req, 65 | HttpStatus httpStatus) { 66 | ErrorInfo info = new ErrorInfo(); 67 | ServletUriComponentsBuilder builder = 68 | ServletUriComponentsBuilder.fromServletMapping(req); 69 | info.setPath(builder.path( 70 | req.getRequestURI()).build().getPath()); 71 | info.setCode(httpStatus.value()); 72 | info.setType(httpStatus.getReasonPhrase()); 73 | return info; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/basaki/error/exception/DataNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.basaki.error.exception; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | import lombok.ToString; 7 | 8 | /** 9 | * {@code DataNotFoundException} exception is thrown when no item is found 10 | * during databsase look up. 11 | *

12 | * 13 | * @author Indra Basak 14 | * @since 12/28/16 15 | */ 16 | @NoArgsConstructor 17 | @ToString(callSuper = true) 18 | @Getter 19 | @Setter 20 | public class DataNotFoundException extends RuntimeException { 21 | 22 | public DataNotFoundException(String message) { 23 | super(message); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/basaki/error/exception/DatabaseException.java: -------------------------------------------------------------------------------- 1 | package com.basaki.error.exception; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | import lombok.ToString; 7 | 8 | /** 9 | * {@code DatabaseException} exception is thrown when database encounters an 10 | * exception while performing an operation. 11 | *

12 | * 13 | * @author Indra Basak 14 | * @since 12/28/16 15 | */ 16 | @NoArgsConstructor 17 | @ToString(callSuper = true) 18 | @Getter 19 | @Setter 20 | public class DatabaseException extends RuntimeException { 21 | 22 | public DatabaseException(String message) { 23 | super(message); 24 | } 25 | 26 | public DatabaseException(String message, Throwable cause) { 27 | super(message, cause); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/basaki/model/BookRequest.java: -------------------------------------------------------------------------------- 1 | package com.basaki.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import lombok.Getter; 6 | 7 | /** 8 | * {@code BookRequest} represents a response during book creation. 9 | *

10 | * 11 | * @author Indra Basak 12 | * @since 12/7/17 13 | */ 14 | @Getter 15 | public class BookRequest { 16 | 17 | private String title; 18 | 19 | private String author; 20 | 21 | @JsonCreator 22 | public BookRequest(@JsonProperty("title") String title, 23 | @JsonProperty("author") String author) { 24 | this.title = title; 25 | this.author = author; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/basaki/service/BookService.java: -------------------------------------------------------------------------------- 1 | package com.basaki.service; 2 | 3 | import com.basaki.annotation.CustomAnnotation; 4 | import com.basaki.data.entity.Book; 5 | import com.basaki.data.repository.BookRepository; 6 | import com.basaki.error.exception.DataNotFoundException; 7 | import com.basaki.model.BookRequest; 8 | import java.util.UUID; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Service; 12 | import org.springframework.transaction.annotation.Transactional; 13 | import org.springframework.util.Assert; 14 | 15 | /** 16 | * {@code BookService} provides CRUD functioanality on book. 17 | *

18 | * 19 | * @author Indra Basak 20 | * @since 11/20/17 21 | */ 22 | @Service 23 | @Slf4j 24 | public class BookService { 25 | 26 | private BookRepository repository; 27 | 28 | @Autowired 29 | public BookService(BookRepository repository) { 30 | this.repository = repository; 31 | } 32 | 33 | @Transactional 34 | public Book create(BookRequest request) { 35 | Book entity = validateRequest(request); 36 | return repository.save(entity); 37 | } 38 | 39 | public Book read(UUID id) { 40 | try { 41 | return repository.getOne(id); 42 | } catch (Exception e) { 43 | throw new DataNotFoundException( 44 | "Book with ID " + id + " not found."); 45 | } 46 | } 47 | 48 | @CustomAnnotation(description = "Validates book request.") 49 | private Book validateRequest(BookRequest request) { 50 | log.info("Validating book request!"); 51 | 52 | Assert.notNull(request, "Book request cannot be empty!"); 53 | Assert.notNull(request.getTitle(), "Book title cannot be missing!"); 54 | Assert.notNull(request.getAuthor(), "Book author cannot be missing!"); 55 | 56 | Book entity = new Book(); 57 | entity.setTitle(request.getTitle()); 58 | entity.setAuthor(request.getAuthor()); 59 | 60 | return entity; 61 | } 62 | } -------------------------------------------------------------------------------- /src/main/resources/config/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | 4 | 5 | # For Spring Actuator /info endpoint 6 | info: 7 | artifact: spring-source-weaving-example 8 | name: spring-source-weaving-example 9 | description: Spring Source Weaving Example 10 | version: 1.0.0 11 | 12 | #Exposes Spring actuator endpoints 13 | management: 14 | health: 15 | diskspace: 16 | enabled: true 17 | db: 18 | enabled: true 19 | defaults: 20 | enabled: true 21 | details: 22 | enabled: true 23 | application: 24 | enabled: true 25 | endpoint: 26 | health: 27 | enabled: true 28 | show-details: true 29 | endpoints: 30 | web: 31 | base-path: / 32 | expose: "*" 33 | 34 | 35 | 36 | #logging: 37 | # level: 38 | # org.springframework: DEBUG 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/main/resources/db/create-db.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE book 2 | ( 3 | id BINARY(16) PRIMARY KEY, 4 | title VARCHAR(32) NOT NULL, 5 | author VARCHAR(32) NOT NULL 6 | ); -------------------------------------------------------------------------------- /src/test/java/com/basaki/config/SwaggerConfigurationFunctionalTests.java: -------------------------------------------------------------------------------- 1 | package com.basaki.config; 2 | 3 | import com.basaki.Application; 4 | import io.restassured.http.ContentType; 5 | import io.restassured.response.Response; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.context.ApplicationContext; 12 | import org.springframework.test.context.ActiveProfiles; 13 | import org.springframework.test.context.junit4.SpringRunner; 14 | 15 | import static io.restassured.RestAssured.given; 16 | import static junit.framework.TestCase.assertEquals; 17 | import static junit.framework.TestCase.assertNotNull; 18 | 19 | /** 20 | * {@code SwaggerConfigurationIntegrationTests} represents functional tests for 21 | * {@code SwaggerConfiguration}. 22 | *

23 | * 24 | * @author Indra Basak 25 | * @since 02/11/18 26 | */ 27 | @RunWith(SpringRunner.class) 28 | @SpringBootTest(classes = {Application.class}, 29 | webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 30 | @ActiveProfiles("test") 31 | public class SwaggerConfigurationFunctionalTests { 32 | 33 | @Value("${local.server.port}") 34 | private Integer port; 35 | 36 | @Autowired 37 | ApplicationContext context; 38 | 39 | @Test 40 | public void testApi() { 41 | Response response = given() 42 | .contentType(ContentType.JSON) 43 | .baseUri("http://localhost") 44 | .port(port) 45 | .contentType(ContentType.JSON) 46 | .get("/v2/api-docs?group=book"); 47 | 48 | assertNotNull(response); 49 | assertEquals(200, response.getStatusCode()); 50 | assertNotNull(response.getBody().prettyPrint()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/com/basaki/controller/BookControllerFunctionalTests.java: -------------------------------------------------------------------------------- 1 | package com.basaki.controller; 2 | 3 | import com.basaki.Application; 4 | import com.basaki.data.entity.Book; 5 | import com.basaki.model.BookRequest; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import io.restassured.http.ContentType; 8 | import io.restassured.response.Response; 9 | import java.io.IOException; 10 | import java.util.UUID; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.beans.factory.annotation.Qualifier; 15 | import org.springframework.beans.factory.annotation.Value; 16 | import org.springframework.boot.test.context.SpringBootTest; 17 | import org.springframework.test.context.ActiveProfiles; 18 | import org.springframework.test.context.junit4.SpringRunner; 19 | 20 | import static io.restassured.RestAssured.given; 21 | import static junit.framework.TestCase.assertEquals; 22 | import static junit.framework.TestCase.assertNotNull; 23 | 24 | /** 25 | * {@code BookControllerFunctionalTests} represents functional tests for {@code 26 | * BookController}. 27 | *

28 | * 29 | * @author Indra Basak 30 | * @since 02/10/18 31 | */ 32 | @RunWith(SpringRunner.class) 33 | @SpringBootTest(classes = {Application.class}, 34 | webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 35 | @ActiveProfiles("test") 36 | public class BookControllerFunctionalTests { 37 | 38 | @Value("${local.server.port}") 39 | private Integer port; 40 | 41 | @Autowired 42 | @Qualifier("customObjectMapper") 43 | private ObjectMapper objectMapper; 44 | 45 | @Test 46 | public void testCreateAndRead() throws IOException { 47 | BookRequest bookRequest = new BookRequest("Indra's Chronicle", "Indra"); 48 | 49 | Response response = given() 50 | .contentType(ContentType.JSON) 51 | .baseUri("http://localhost") 52 | .port(port) 53 | .contentType(ContentType.JSON) 54 | .body(bookRequest) 55 | .post("/books"); 56 | assertNotNull(response); 57 | assertEquals(201, response.getStatusCode()); 58 | Book bookCreate = 59 | objectMapper.readValue(response.getBody().prettyPrint(), 60 | Book.class); 61 | assertNotNull(bookCreate); 62 | assertNotNull(bookCreate.getId()); 63 | assertEquals(bookRequest.getTitle(), bookCreate.getTitle()); 64 | assertEquals(bookRequest.getAuthor(), bookCreate.getAuthor()); 65 | 66 | response = given() 67 | .baseUri("http://localhost") 68 | .port(port) 69 | .contentType(ContentType.JSON) 70 | .get("/books/" + bookCreate.getId().toString()); 71 | 72 | assertNotNull(response); 73 | assertEquals(200, response.getStatusCode()); 74 | 75 | Book bookRead = objectMapper.readValue(response.getBody().prettyPrint(), 76 | Book.class); 77 | assertNotNull(bookRead); 78 | assertEquals(bookCreate.getId(), bookRead.getId()); 79 | assertEquals(bookCreate.getAuthor(), bookRead.getAuthor()); 80 | assertEquals(bookCreate.getTitle(), bookRead.getTitle()); 81 | assertEquals(bookCreate.getAuthor(), bookRead.getAuthor()); 82 | } 83 | 84 | //@Test 85 | public void testDataNotFoundRead() { 86 | Response response = given() 87 | .baseUri("http://localhost") 88 | .port(port) 89 | .contentType(ContentType.JSON) 90 | .get("/books/" + UUID.randomUUID().toString()); 91 | 92 | assertNotNull(response); 93 | assertEquals(404, response.getStatusCode()); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/test/java/com/basaki/controller/BookControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.basaki.controller; 2 | 3 | import com.basaki.data.entity.Book; 4 | import com.basaki.error.exception.DataNotFoundException; 5 | import com.basaki.model.BookRequest; 6 | import com.basaki.service.BookService; 7 | import java.util.UUID; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | import org.mockito.MockitoAnnotations; 13 | 14 | import static org.junit.Assert.assertEquals; 15 | import static org.junit.Assert.assertNotNull; 16 | import static org.mockito.ArgumentMatchers.any; 17 | import static org.mockito.Mockito.when; 18 | 19 | /** 20 | * {@code BookControllerTest} represents unit test for {@code 21 | * BookController}. 22 | *

23 | * 24 | * @author Indra Basak 25 | * @since 02/10/18 26 | */ 27 | public class BookControllerTest { 28 | 29 | @Mock 30 | private BookService service; 31 | 32 | @InjectMocks 33 | private BookController controller; 34 | 35 | @Before 36 | public void setUp() { 37 | MockitoAnnotations.initMocks(this); 38 | } 39 | 40 | @Test 41 | public void testCreate() { 42 | Book book = new Book(); 43 | book.setId(UUID.randomUUID()); 44 | book.setTitle("Indra's Chronicle"); 45 | book.setAuthor("Indra"); 46 | when(service.create(any(BookRequest.class))).thenReturn(book); 47 | 48 | BookRequest request = new BookRequest("Indra's Chronicle", "Indra"); 49 | Book result = controller.create(request); 50 | assertNotNull(result); 51 | assertEquals(book, result); 52 | } 53 | 54 | @Test 55 | public void testRead() { 56 | Book book = new Book(); 57 | book.setId(UUID.randomUUID()); 58 | book.setTitle("Indra's Chronicle"); 59 | book.setAuthor("Indra"); 60 | when(service.read(any(UUID.class))).thenReturn(book); 61 | 62 | Book result = controller.read(UUID.randomUUID()); 63 | assertNotNull(result); 64 | assertEquals(book, result); 65 | } 66 | 67 | @Test(expected = DataNotFoundException.class) 68 | public void testDataNotFoundRead() { 69 | when(service.read(any(UUID.class))).thenThrow( 70 | new DataNotFoundException("Not Found!")); 71 | 72 | controller.read(UUID.randomUUID()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/test/java/com/basaki/controller/CustomErrorControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.basaki.controller; 2 | 3 | import com.basaki.error.ErrorInfo; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.mockito.InjectMocks; 9 | import org.mockito.Mock; 10 | import org.mockito.MockitoAnnotations; 11 | import org.springframework.boot.web.servlet.error.ErrorAttributes; 12 | import org.springframework.mock.web.MockHttpServletRequest; 13 | import org.springframework.mock.web.MockHttpServletResponse; 14 | import org.springframework.web.context.request.RequestContextHolder; 15 | import org.springframework.web.context.request.ServletRequestAttributes; 16 | import org.springframework.web.context.request.WebRequest; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | import static org.junit.Assert.assertNotNull; 20 | import static org.mockito.ArgumentMatchers.any; 21 | import static org.mockito.ArgumentMatchers.anyBoolean; 22 | import static org.mockito.Mockito.when; 23 | 24 | /** 25 | * {@code CustomErrorControllerTest} represents unit test for {@code 26 | * CustomErrorController}. 27 | *

28 | * 29 | * @author Indra Basak 30 | * @since 02/11/18 31 | */ 32 | public class CustomErrorControllerTest { 33 | 34 | @Mock 35 | private ErrorAttributes errorAttributes; 36 | 37 | @InjectMocks 38 | private CustomErrorController controller; 39 | 40 | private MockHttpServletRequest request; 41 | 42 | private MockHttpServletResponse response; 43 | 44 | @Before 45 | public void setUp() { 46 | MockitoAnnotations.initMocks(this); 47 | request = new MockHttpServletRequest(); 48 | RequestContextHolder.setRequestAttributes( 49 | new ServletRequestAttributes(request)); 50 | 51 | response = new MockHttpServletResponse(); 52 | } 53 | 54 | @Test 55 | public void testError() { 56 | Map attributes = new HashMap<>(); 57 | attributes.put("message", "test-message"); 58 | attributes.put("error", "test-error"); 59 | 60 | when(errorAttributes.getErrorAttributes(any(WebRequest.class), 61 | anyBoolean())).thenReturn(attributes); 62 | 63 | ErrorInfo errorInfo = controller.error(request, response); 64 | assertNotNull(errorInfo); 65 | assertEquals("test-message", errorInfo.getMessage()); 66 | assertNotNull(controller.getErrorPath()); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/com/basaki/error/ExceptionProcessorTest.java: -------------------------------------------------------------------------------- 1 | package com.basaki.error; 2 | 3 | import com.basaki.error.exception.DataNotFoundException; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.mock.web.MockHttpServletRequest; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | 11 | /** 12 | * {@code ExceptionProcessorTest} represents unit test for {@code 13 | * ExceptionProcessor}. 14 | *

15 | * 16 | * @author Indra Basak 17 | * @since 02/11/18 18 | */ 19 | public class ExceptionProcessorTest { 20 | 21 | private ExceptionProcessor processor; 22 | private MockHttpServletRequest request; 23 | private String message; 24 | private String path; 25 | 26 | @Before 27 | public void setUp() throws Exception { 28 | processor = new ExceptionProcessor(); 29 | request = new MockHttpServletRequest(); 30 | message = "some message"; 31 | path = "/some/path"; 32 | request.setRequestURI(path); 33 | request.setPathInfo(path); 34 | } 35 | 36 | @Test 37 | public void testHandleDataNotFoundException() throws Exception { 38 | DataNotFoundException exception = new DataNotFoundException(message); 39 | ErrorInfo info = 40 | processor.handleDataNotFoundException(request, exception); 41 | validate(info, HttpStatus.NOT_FOUND, message); 42 | } 43 | 44 | private void validate(ErrorInfo info, HttpStatus status, String msg) { 45 | assertEquals(msg, info.getMessage()); 46 | assertEquals(status.value(), info.getCode()); 47 | assertEquals(status.getReasonPhrase(), info.getType()); 48 | assertEquals(path, info.getPath()); 49 | } 50 | } 51 | --------------------------------------------------------------------------------