├── .gitignore ├── README.md ├── application ├── pom.xml └── src │ └── main │ ├── java │ ├── com │ │ └── okta │ │ │ └── developer │ │ │ ├── BirdController.java │ │ │ └── SpringBootModulesApplication.java │ └── module-info.java │ └── resources │ └── application.properties ├── docker-compose.yml ├── persistence ├── pom.xml └── src │ └── main │ └── java │ ├── com │ └── okta │ │ └── developer │ │ └── animals │ │ └── bird │ │ ├── Bird.java │ │ ├── BirdPersistence.java │ │ └── BirdRepository.java │ └── module-info.java └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | target 4 | application/src/main/resources/application-local.properties -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot Using Java Modules 2 | 3 | Java is one of the most mature and persistent development languages that exist. Recently it entered into a 6-month release schedule which enabled to deliver more frequent updates to the language. 4 | One of those changes was the modular system that is available since Java 9. 5 | 6 | The Modular system came to address a recurring concern on Java apps: 'how to hide classes from libraries that are not meant to be used outside or just by specific applications?' 7 | We know that we have the visibility modifiers: public, private, protected and default, but those are not enough to provide external visibility. It is common for a class to live inside a package and be used throughout the library, but it may be a class not meant for external use. Therefore, it has public visibility but on the other side, it shouldn't be available for applications depending on that library. This is the situation in which the Modular System can help. 8 | 9 | 10 | ## Introduction 11 | 12 | Starting on Java 9 the JDK went under a major refactoring to modularize its content. Various modules were created to organize the contents, some examples are `java.base`, `java.sql`, `java.xml` and others. To have an idea there are a total of 60 modules in Java 14 JDK. 13 | 14 | `java.base` has the fundamental classes like `Object`, `String`, `Integer`, `Double`, etc 15 | 16 | `java.sql` has classes related to accessing the JDBC API like `ResultSet` , `Connection` and others 17 | 18 | `java.xml` has classes related to XML manipulation like `XMLStreamReader`, `XMLStreamWriter` and others 19 | 20 | The modularization also enabled the possibility of reducing the Java runtime to include, let's say, just the `java.base` if your application depends just on this module. By using the `jlink` tool, that is bundled with the JDK, you can create a micro runtime with just the JDK modules you need. I'll not cover how to use `jlink` as it is not the focus but you can see an example on this [Baeldung article](https://www.baeldung.com/jlink) 21 | 22 | To go through this article you should have at least some basic understanding of Spring Boot, Maven, REST web services principles and Docker installed. 23 | 24 | ## Install a Java 9+ JDK 25 | 26 | First, you'll need a Java 9+ JDK to use modules. If you have been using Java 8 then you'll probably have to download a separate JDK with a version of 9 or later to be used on this tutorial. The project is set up to use JDK 11 in this tutorial. You can download the JDKs from [AdoptOpenJDK](https://adoptopenjdk.net/). Just make sure your JAVA_HOME environment variable is pointing to that JDK. 27 | 28 | ## Project Structure 29 | 30 | In this article, we'll be covering how to develop a simple application with two modules: the `application` module that contains the web-facing classes and the `persistence` module that contains the data access layer. We'll also be using a couple of dependencies to illustrate how to use those on a modular application: `spring-boot-starter-data-mongodb` and `okta-spring-boot-starter` 31 | 32 | The project source code can be found at [GitHub](https://github.com/brunocleite/spring-boot-using-java-modules) 33 | 34 | ### How to Structure a Modular Project with Maven? 35 | 36 | Let's create this project folder structure manually to better understand how it should be structured. Each module will live inside a separate directory and have it's own `pom.xml` file. There will also be a `pom.xml` on the project root that will serve as the parent pom for the modules. You can create the following folder structure: 37 | 38 | ``` 39 | . 40 | ├── application 41 | │   ├── pom.xml 42 | │   └── src 43 | │   └── main 44 | │   ├── java 45 | │   └── resources 46 | ├── persistence 47 | │   └── src 48 | │   └── main 49 | │   └── java 50 | └── pom.xml 51 | ``` 52 | 53 | ### How to Tie Up the Three pom.xml Files with Maven? 54 | 55 | First, let's define the root `pom.xml`. It will contain the common `` indication to `spring-boot-started-parent` and two entries on `` section, that are the name of the directories for the modules we are developing. Please note that those are specific to Maven and specify sub-projects and have nothing to do with the Java modules that we'll be working on later. 56 | 57 | ```xml 58 | 59 | 61 | 4.0.0 62 | 63 | org.springframework.boot 64 | spring-boot-starter-parent 65 | 2.3.1.RELEASE 66 | 67 | 68 | com.okta.developer 69 | spring-boot-with-modules 70 | 0.0.1-SNAPSHOT 71 | pom 72 | 73 | 74 | 11 75 | 76 | 77 | 78 | application 79 | persistence 80 | 81 | 82 | 83 | ``` 84 | 85 | The `application` module will have a pom.xml like below, pointing to the parent pom.xml that was described above. It will also have a dependency on `spring-boot-starter-web` because we'll be creating some REST endpoints on it and also a dependency on our 'persistence' module that will be described next. 86 | 87 | ```xml 88 | 89 | 91 | 4.0.0 92 | 93 | com.okta.developer 94 | spring-boot-with-modules 95 | 0.0.1-SNAPSHOT 96 | 97 | spring-boot-with-modules-app 98 | 99 | 100 | 101 | org.springframework.boot 102 | spring-boot-starter-web 103 | 104 | 105 | com.okta.developer 106 | spring-boot-with-modules-persistence 107 | ${project.version} 108 | 109 | 110 | 111 | 112 | 113 | 114 | org.springframework.boot 115 | spring-boot-maven-plugin 116 | 117 | 118 | 119 | 120 | 121 | ``` 122 | 123 | At last, we have the `persistence` module which will have a `pom.xml` like the one below, also pointing to the parent pom.xml that was first defined. This one will have a dependency on `spring-data-mongo` as we'll be saving our data to a Mongo DB. 124 | 125 | ```xml 126 | 127 | 129 | 4.0.0 130 | 131 | com.okta.developer 132 | spring-boot-with-modules 133 | 0.0.1-SNAPSHOT 134 | 135 | spring-boot-with-modules-persistence 136 | 137 | 138 | 139 | org.springframework.boot 140 | spring-boot-starter-data-mongodb 141 | 142 | 143 | 144 | 145 | ``` 146 | 147 | This project structure is enough for compiling if you go to the project root and run `mvn compile`. 148 | 149 | **NOTE:** Please don't confuse Maven modules with Java Modules. 150 | 151 | **Maven modules** are used to separate a project into multiple sub-projects. The main project will have a `pom.xml` referencing sub-projects on `` section. Each sub-project will have its own `pom.xml`. When building the main project it will automatically build the sub-projects too. 152 | 153 | **Java modules** is another name for JPMS (Java Platform Module System), added on JDK 9 under the name Project Jigsaw. It allows applications (packaged as JAR or WAR) to define a `module-info.java` file on its root with directives that control which classes the application will allow others to access and which other modules it needs on compile or runtime. 154 | 155 | ## Writing Initial Application Without Java Modules 156 | 157 | Let's write some classes without using the Java modules so we can afterward include the module definitions and see the differences. 158 | What defines that an application is using Java modules is the presence of `module-info.java` on its source root. We'll be creating this later on. 159 | 160 | ### Persistence Module 161 | 162 | Create a class `Bird` on the Persistence module for representing the entity that we'll be saving to DB. 163 | This class will be stored on `persistence/src/main/java/com/okta/developer/animals/bird/Bird.java` 164 | 165 | ```java 166 | package com.okta.developer.animals.bird; 167 | 168 | import org.springframework.data.annotation.Id; 169 | 170 | public class Bird { 171 | 172 | @Id 173 | private String id; 174 | 175 | private String specie; 176 | private String size; 177 | 178 | public String getId() { 179 | return id; 180 | } 181 | 182 | public void setId(String id) { 183 | this.id = id; 184 | } 185 | 186 | public String getSpecie() { 187 | return specie; 188 | } 189 | 190 | public void setSpecie(String specie) { 191 | this.specie = specie; 192 | } 193 | 194 | public String getSize() { 195 | return size; 196 | } 197 | 198 | public void setSize(String size) { 199 | this.size = size; 200 | } 201 | } 202 | 203 | ``` 204 | 205 | Now we need to create a repository to save this entity to DB. `Spring Data MongoDB` does that for us automatically creating the CRUD operations so we just have to create an interface extending MongoRepository. 206 | This class will be stored on `persistence/src/main/java/com/okta/developer/animals/bird/BirdRepository.java` 207 | ```java 208 | package com.okta.developer.animals.bird; 209 | 210 | import org.springframework.data.mongodb.repository.MongoRepository; 211 | 212 | public interface BirdRepository extends MongoRepository { 213 | } 214 | ``` 215 | 216 | At last, for the persistence module, we'll be creating a service class to expose the persistence operations. 217 | This class will be stored on `persistence/src/main/java/com/okta/developer/animals/bird/BirdPersistence.java` 218 | 219 | ```java 220 | package com.okta.developer.animals.bird; 221 | 222 | import org.springframework.beans.factory.annotation.Autowired; 223 | import org.springframework.stereotype.Component; 224 | 225 | import javax.annotation.PostConstruct; 226 | import java.util.List; 227 | 228 | @Component 229 | public class BirdPersistence { 230 | 231 | private BirdRepository birdRepository; 232 | 233 | @Autowired 234 | public BirdPersistence(BirdRepository birdRepository) { 235 | this.birdRepository = birdRepository; 236 | } 237 | 238 | @PostConstruct 239 | void postConstruct(){ 240 | Bird sampleBird = new Bird(); 241 | sampleBird.setSpecie("Hummingbird"); 242 | sampleBird.setSize("small"); 243 | save(sampleBird); 244 | } 245 | 246 | public void save(Bird bird) { 247 | birdRepository.save(bird); 248 | } 249 | 250 | public List get() { 251 | return birdRepository.findAll(); 252 | } 253 | } 254 | 255 | ``` 256 | 257 | 258 | ### Application Module 259 | 260 | Now on the application module create the main application class annotated with `@SpringBootApplication`. 261 | This class will be stored on `application/src/main/java/com/okta/developer/SpringBootModulesApplication.java` 262 | 263 | ```java 264 | package com.okta.developer; 265 | 266 | import org.springframework.boot.SpringApplication; 267 | import org.springframework.boot.autoconfigure.SpringBootApplication; 268 | 269 | @SpringBootApplication 270 | public class SpringBootModulesApplication { 271 | 272 | public static void main(String[] args) { 273 | SpringApplication.run(SpringBootModulesApplication.class, args); 274 | } 275 | } 276 | 277 | ``` 278 | 279 | And now a controller to expose REST operations on the Bird classes 280 | This class will be stored on `application/src/main/java/com/okta/developer/BirdController.java` 281 | 282 | ```java 283 | package com.okta.developer; 284 | 285 | import com.okta.developer.animals.bird.Bird; 286 | import com.okta.developer.animals.bird.BirdPersistence; 287 | import org.springframework.web.bind.annotation.GetMapping; 288 | import org.springframework.web.bind.annotation.PostMapping; 289 | import org.springframework.web.bind.annotation.RequestBody; 290 | import org.springframework.web.bind.annotation.RestController; 291 | 292 | import java.util.List; 293 | 294 | @RestController 295 | public class BirdController { 296 | 297 | private BirdPersistence birdPersistence; 298 | 299 | public BirdController(BirdPersistence birdPersistence) { 300 | this.birdPersistence = birdPersistence; 301 | } 302 | 303 | @GetMapping("bird") 304 | public List getBird() { 305 | return birdPersistence.get(); 306 | } 307 | 308 | @PostMapping("bird") 309 | public void saveBird(@RequestBody Bird bird) { 310 | birdPersistence.save(bird); 311 | } 312 | 313 | } 314 | 315 | ``` 316 | 317 | At this point, the application is functional and can be run. First, start a MongoDB instance using the following docker command: 318 | ```bash 319 | docker run -p 27017:27017 mongo:3.6-xenial 320 | ``` 321 | Then go to the project root and run: 322 | ```bash 323 | mvn install && mvn spring-boot:run -pl application 324 | ``` 325 | 326 | If everything went correctly you'll be able to navigate to `http://localhost:8080/bird` and see a JSON output like the following: 327 | ```json 328 | [{"id":"5f03ff7277a08a55ae73c8b9","specie":"Hummingbird","size":"small"}] 329 | ``` 330 | 331 | ## Making the Application Secure 332 | 333 | Let's tune our app and make it secure so we start depending on another external library before moving on to adding Java modules. 334 | 335 | Add the following dependency to your `application/pom.xml` file: 336 | ```xml 337 | 338 | com.okta.spring 339 | okta-spring-boot-starter 340 | 1.3.0 341 | 342 | ``` 343 | 344 | ### Register an Application on Okta 345 | 346 | To begin, sign up for a [forever-free Okta developer account](https://developer.okta.com/signup/). 347 | 348 | Once you're signed in to Okta, register your client application. 349 | 350 | * In the top menu, click on **Applications** 351 | * Click on **Add Application** 352 | * Select **Web** and click **Next** 353 | * Enter `Spring Boot with Java Modules` for the **Name** (this value doesn't matter, so feel free to change it) 354 | * Change the Login redirect URI to be `http://localhost:8080/login/oauth2/code/okta` 355 | * Click **Done** 356 | 357 | ### Configure the App with Okta Information 358 | 359 | Create a file `application/src/main/resources/application.properties` with the following content: 360 | 361 | ``` 362 | okta.oauth2.issuer=https://{yourOktaDomain}/oauth2/default 363 | okta.oauth2.clientId={clientId} 364 | okta.oauth2.clientSecret={clientSecret} 365 | ``` 366 | 367 | You can find {clientId} and {clientSecret} in your application setup. 368 | 369 | The {yourOktaDomain} you can find on Okta dashboard. 370 | 371 | Now if you restart the app and navigate to `http://localhost:8080/bird` you'll get a login page appearing. 372 | 373 | ## Using Java Modules 374 | 375 | Now it is time to modularize the app. This is achieved by placing a file `module-info.java` on source root for each module. We'll be doing this for our two modules `application` and `persistence`. There are two ways to modularize a Java app: top-down and bottom-up. In this tutorial we'll be showing the bottom-up approach, that is modularizing the libraries before the app. This approach is preferable as we'll have `persistence` already modularized when writing the `application` `module-info.java`. If `application` was modularized first then `persistence` would be treated as an automatic module, and it would have to be referenced as the JAR file name. 376 | 377 | ### Modularize `persistence` Library 378 | 379 | Create a module declaration file `persistence/src/main/java/module-info.java` with the following content: 380 | 381 | ```java 382 | module com.okta.developer.modules.persistence { 383 | 384 | requires java.annotation; 385 | requires spring.beans; 386 | requires spring.context; 387 | requires spring.data.commons; 388 | requires spring.data.mongodb; 389 | 390 | exports com.okta.developer.animals.bird; 391 | } 392 | ``` 393 | 394 | Each `requires` keyword signalize that this module will be depending on some other module. 395 | Spring, on version 5, is not modularized yet so its JAR files don't have the `module-info.java`. 396 | When you have a dependency on the `modulepath` (former classpath for non-modular applications) like this they will be available as `automatic modules`. 397 | 398 | An `automatic module` gets its name from the property `Automatic-Module-Name` inside `MANIFEST.MF` or from the `JAR` filename itself if that is absent. 399 | 400 | On Spring 5 the team was nice enough to put the `Automatic-Module-Name` for all the libraries. So those are the names used on our persistence app dependencies: `spring.beans`, `spring.context`, `spring.data.commons`, `spring.data.mongodb`. 401 | 402 | The `exports` keyword exports all classes in the package. When another module uses a `requires` clause referencing this one it will have access to those classes. 403 | 404 | In this example, the module is exporting all classes under `com.okta.developer.animals.bird` package. 405 | 406 | ### Modularize `application` App 407 | 408 | Create a module declaration file `application/src/main/java/module-info.java` with the following content: 409 | 410 | ```java 411 | module com.okta.developer.modules.app { 412 | 413 | requires com.okta.developer.modules.persistence; 414 | 415 | requires spring.web; 416 | requires spring.boot; 417 | requires spring.boot.autoconfigure; 418 | } 419 | ``` 420 | 421 | This one is similar to the first one but besides the Spring dependencies we also have the `com.okta.developer.modules.persistence` dependency. That is the other module we have developed. 422 | 423 | By adding the `requires com.okta.developer.modules.persistence` this module will have access to the package that was exported `com.okta.developer.animals.bird`. 424 | 425 | 426 | ## Running the App 427 | 428 | Go to the project root and run 429 | ```bash 430 | mvn install && mvn spring-boot:run -pl application 431 | ``` 432 | 433 | Again, if everything went correctly you'll be able, after logging in, to navigate to `http://localhost:8080/bird` and see a JSON output. 434 | 435 | ## Learning More About Java Modular System 436 | 437 | The Java Modular System is an excellent addition to the Java ecosystem, it helps organize and isolate classes that were otherwise exposed without need. By looking at the application `module-info.java` it is possible to have a blueprint of its dependencies. 438 | 439 | The topic is broad and if you want to learn more this [talk by Alex Buckley](https://www.youtube.com/watch?v=22OW5t_Mbnk) is an excellent start. 440 | 441 | In case you have an existing Spring Boot application and want to make it use the modular system then this [talk by Jaap Coomans](https://www.youtube.com/watch?v=hxsCYxZ1gXU) covers it nicely. 442 | 443 | If you have any questions about this post, please add a comment below. For more awesome content, follow @oktadev on Twitter, like us on Facebook, or subscribe to our YouTube channel. -------------------------------------------------------------------------------- /application/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.okta.developer 7 | spring-boot-with-modules 8 | 0.0.1-SNAPSHOT 9 | 10 | spring-boot-with-modules-app 11 | 12 | 13 | 14 | org.springframework.boot 15 | spring-boot-starter-web 16 | 17 | 18 | com.okta.spring 19 | okta-spring-boot-starter 20 | 1.3.0 21 | 22 | 23 | com.okta.developer 24 | spring-boot-with-modules-persistence 25 | ${project.version} 26 | 27 | 28 | 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-maven-plugin 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /application/src/main/java/com/okta/developer/BirdController.java: -------------------------------------------------------------------------------- 1 | package com.okta.developer; 2 | 3 | import com.okta.developer.animals.bird.Bird; 4 | import com.okta.developer.animals.bird.BirdPersistence; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.PostMapping; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import java.util.List; 11 | 12 | @RestController 13 | public class BirdController { 14 | 15 | private BirdPersistence birdPersistence; 16 | 17 | public BirdController(BirdPersistence birdPersistence) { 18 | this.birdPersistence = birdPersistence; 19 | } 20 | 21 | @GetMapping("bird") 22 | public List getBird() { 23 | return birdPersistence.get(); 24 | } 25 | 26 | @PostMapping("bird") 27 | public void saveBird(@RequestBody Bird bird) { 28 | birdPersistence.save(bird); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /application/src/main/java/com/okta/developer/SpringBootModulesApplication.java: -------------------------------------------------------------------------------- 1 | package com.okta.developer; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class SpringBootModulesApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(SpringBootModulesApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /application/src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module com.okta.developer.modules.app { 2 | 3 | requires com.okta.developer.modules.persistence; 4 | 5 | requires spring.web; 6 | requires spring.boot; 7 | requires spring.boot.autoconfigure; 8 | 9 | } -------------------------------------------------------------------------------- /application/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | okta.oauth2.issuer=https://{yourOktaDomain}/oauth2/default 2 | okta.oauth2.clientId={clientID} 3 | okta.oauth2.clientSecret={clientSecret} -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | db: 5 | image: mongo:3.6-xenial 6 | ports: 7 | - "27017:27017" 8 | container_name: spring-boot-with-java-modules-mongo -------------------------------------------------------------------------------- /persistence/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.okta.developer 7 | spring-boot-with-modules 8 | 0.0.1-SNAPSHOT 9 | 10 | spring-boot-with-modules-persistence 11 | 12 | 13 | 14 | org.springframework.boot 15 | spring-boot-starter-data-mongodb 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /persistence/src/main/java/com/okta/developer/animals/bird/Bird.java: -------------------------------------------------------------------------------- 1 | package com.okta.developer.animals.bird; 2 | 3 | import org.springframework.data.annotation.Id; 4 | 5 | public class Bird { 6 | 7 | @Id 8 | private String id; 9 | 10 | private String specie; 11 | private String size; 12 | 13 | public String getId() { 14 | return id; 15 | } 16 | 17 | public void setId(String id) { 18 | this.id = id; 19 | } 20 | 21 | public String getSpecie() { 22 | return specie; 23 | } 24 | 25 | public void setSpecie(String specie) { 26 | this.specie = specie; 27 | } 28 | 29 | public String getSize() { 30 | return size; 31 | } 32 | 33 | public void setSize(String size) { 34 | this.size = size; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /persistence/src/main/java/com/okta/developer/animals/bird/BirdPersistence.java: -------------------------------------------------------------------------------- 1 | package com.okta.developer.animals.bird; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Component; 5 | 6 | import javax.annotation.PostConstruct; 7 | import java.util.List; 8 | 9 | @Component 10 | public class BirdPersistence { 11 | 12 | private BirdRepository birdRepository; 13 | 14 | @Autowired 15 | public BirdPersistence(BirdRepository birdRepository) { 16 | this.birdRepository = birdRepository; 17 | } 18 | 19 | @PostConstruct 20 | void postConstruct(){ 21 | birdRepository.deleteAll(); //Clean up DB so there is no leftover data between runs 22 | Bird sampleBird = new Bird(); 23 | sampleBird.setSpecie("Hummingbird"); 24 | sampleBird.setSize("small"); 25 | save(sampleBird); 26 | } 27 | 28 | public void save(Bird bird) { 29 | birdRepository.save(bird); 30 | } 31 | 32 | public List get() { 33 | return birdRepository.findAll(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /persistence/src/main/java/com/okta/developer/animals/bird/BirdRepository.java: -------------------------------------------------------------------------------- 1 | package com.okta.developer.animals.bird; 2 | 3 | import org.springframework.data.mongodb.repository.MongoRepository; 4 | 5 | public interface BirdRepository extends MongoRepository { 6 | 7 | 8 | } 9 | -------------------------------------------------------------------------------- /persistence/src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module com.okta.developer.modules.persistence { 2 | 3 | requires java.annotation; 4 | requires spring.beans; 5 | requires spring.context; 6 | requires spring.data.commons; 7 | requires spring.data.mongodb; 8 | 9 | exports com.okta.developer.animals.bird; 10 | 11 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.3.1.RELEASE 9 | 10 | 11 | com.okta.developer 12 | spring-boot-with-modules 13 | 0.0.1-SNAPSHOT 14 | pom 15 | 16 | 17 | 11 18 | 19 | 20 | 21 | application 22 | persistence 23 | 24 | 25 | --------------------------------------------------------------------------------