├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------
/img/aspectj-src-weaving-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------