Theme NexT works best with JavaScript enabled
0%

Spring Boot Manual


IMPORTANT:
Some of the content here is a personal summary/abbreviation of contents on the Offical Spring Boot Documentation. Feel free to refer to the official site if you think some of the sections written here are not clear.


Spring Boot Installation

For instructions on installing and a quick overview of using Spring Boot, please refer to this page

Structuring Your Code

Technically, Spring Boot does not require any specific code layout to work. However, there are some best practices that help:

  • Your main application class:

    • It is generally recommend that you locate your main application class in a root package above other
      classes.
      The @SpringBootApplication annotation is often placed on your main class, and it implicitly
      defines a base “search package” for certain items

    • For example, the follwoing structure:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      com
      +- example
      +- myapplication
      +- Application.java
      |
      +- customer
      | +- Customer.java
      | +- CustomerController.java
      | +- CustomerService.java
      | +- CustomerRepository.java
      |
      +- order
      +- Order.java
      +- OrderController.java
      +- OrderService.java
      +- OrderRepository.java
  • Your primary configuration class

    • Although it is possible to use SpringApplication with XML sources, we generally recommend that your primary source be a single @Configuration class. Usually the class that defines the main method is a good candidate as the primary @Configuration.
    • You can also import configuration classes:
      • The @Import annotation can be used to import additional configuration classes. Alternatively, you can use @ComponentScan to automatically pick up all Spring components, including @Configuration classes.
    • Import XML Configuration
      • If you need to use an XML based configuration, we recommend that you still start with a @Configuration class. You can then use an @ImportResource annotation to load XML configuration files.

        Note:

        • A single @SpringBootApplication annotation can be used to enable three features, that is:
          • @EnableAutoConfiguration: enable Spring Boot’s auto-configuration mechanism, which *imports many xxxAutoConfiguration classes defined by @Import(AutoConfigurationImportSelector.class) *(you will see this when you look at the @EnableAutoConfigurations class). More details see the section below.
          • @ComponentScan: enable @Component scan on the package where the application is located (see the best practices)
          • @Configuration: allow to register extra beans in the context or import additional configuration classes
            SO, the @SpringBootApplication annotation is equivalent to using @Configuration, @EnableAutoConfiguration, and @ComponentScan with their default attributes, as shown in the following example:
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.myapplication;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication // same as @Configuration @EnableAutoConfiguration @ComponentScan
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

}

Auto-Configuration

(additional reference: https://www.javatpoint.com/spring-boot-auto-configuration)

You can also choose to use the Spring Boot auto-configuration, which attempts to automatically configure your Spring application based on the jar dependencies that you have added.

For example, if the H2 database Jar is present in the classpath and we have not configured any beans related to the database manually, the Spring Boot’s auto-configuration feature automatically configures it in the project.

To enable auto-configuration, you need to add either @EnableAutoConfiguration or @SpringBootApplication annotations to one of your @Configuration classes.

EnableAutoConfiguration under the hood

  1. When you click into the @EnableAutoConfiguration annotation, you will see that it imports the AutoConfigurationImportSelector.class.

  2. Within that class, it uses the method List<String> configurations = SpringFactoriesLoader.loadFactoryNames(..) to return to your configurations that will be imported into your application.

  3. When you click into that method loadFactoryNames(), you will see the line: classLoader.getResources(FACTORIES_RESOURCE_LOCATION), where that Factories_RESOURCE_LOCATION points to: \.m2\repository\org\springframework\boot\spring-boot-autoconfigure\2.3.0.RELEASE\spring-boot-autoconfigure-2.3.0.RELEASE.jar!\META-INF\spring.factories

  4. The xxxAutoConfiguration classes specified in that spring.factories file will be loaded into your application, if conditions specified in those classes are fulfilled (e.g. @ConditionalOnMissingProperties(..))

  5. And it is those xxxAutoConfiguration classes that configures your Spring Boot Application by reading/injecting values from their respective .properties file. This means that you can configure those settings in your own application.properties file with the correct prefix

Viewing Loaded AutoConfiguration Classes

Though you could manually look into each AutoConfiguration class to know whether if it will be loaded, there is an easier way to do so.

  1. You need to enable the debug mode for your application. This can be done by specifying: debug=true in your application.properties or application.yml
  2. Then, if you run your application, you will see, inside the console, the section Auto-Configuration Report, which will tell you all the AutoConfiguration classes that are loaded or not loaded

Disabling an AutoConfiguration Class

However, if you find that specific auto-configuration classes that you do not want are being applied, you can also specify to disable disable the specific auto-configuration classes. We use the exclude attribute of the annotation @EnableAutoConfiguration to disable the auto-configuration classes. For example:

1
2
3
4
5
6
7
8
import org.springframework.boot.autoconfigure.*;  
import org.springframework.boot.autoconfigure.jdbc.*;
import org.springframework.context.annotation.*;

@Configuration(proxyBeanMethods = false)
@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
public class MyConfiguration{
}

We can use the attribute excludeName of the annotation @EnableAutoConfiguration and specify the qualified name of the class, if the class is not on the class path. We can exclude any number of auto-configuration classes by using the property spring.autoconfigure.exclude.

This is also supported if you use the @SpringBootApplication annotation:

1
2
3
4
5
import org.springframework.boot.autoconfigure.*;
import org.springframework.boot.autoconfigure.jdbc.*;
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class}) // also supported
public class MyApplication {
}

Spring Beans and Dependency Injection in Spring Boot

You are free to use any of the standard Spring Framework techniques to define your beans and their injected dependencies. For simplicity, we often find that using @ComponentScan (to find your beans) and using @Autowired (to do constructor injection) works well. If you structure your code as suggested above (locating your application class in a root package), you can add @ComponentScan without any arguments, as it will automatically scan all component classes in current package and all its subpackages. All of your application components (@Component, @Service, @Repository, @Controller etc.) are automatically registered as Spring Beans.

For example, if you have in your sub-package a @Service as shown below, it will be automatically included in the configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class DatabaseAccountService implements AccountService {
private final RiskAssessor riskAssessor; // Notice this will be final

@Autowired
public DatabaseAccountService(RiskAssessor riskAssessor) {
this.riskAssessor = riskAssessor;
}
// ...
}

Notice how using constructor injection lets the riskAssessor field be marked as final, indicating that the reference to that riskAssessor object cannot be subsequently changed (mutator methods of that object can still be used, of course).

Running Your Application

One of the biggest advantages of packaging your application as a jar and using an embedded HTTP server is that you can run your application as you would any other. Debugging Spring Boot applications is also easy. You do not need any special IDE plugins or extensions.

If you use the Spring Boot Maven or Gradle plugins to create an executable jar, you can run your application using java -jar, as shown in the following example:

1
$ java -jar target/myapplication-0.0.1-SNAPSHOT.jar

It is also possible to run a packaged application with remote debugging support enabled. Doing so lets you attach a debugger to your packaged application, as shown in the following example:

1
2
$ java -Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=8000,suspend=n \
-jar target/myapplication-0.0.1-SNAPSHOT.jar

Compiling and Running using Maven Plugin

The Spring Boot Maven plugin includes a run goal that can be used to quickly compile and run your application. Applications run in an exploded form, as they do in your IDE. The following example shows a typical Maven command to run a Spring Boot application:

1
$ mvn spring-boot:run

You might also want to use the MAVEN_OPTS operating system environment variable, as shown in the following example:

1
$ export MAVEN_OPTS=-Xmx1024m

where:

  • -Xmx1024m specifies the maximum size, in bytes, of the memory allocation pool. This value must a multiple of 1024 greater than 2 MB. Append the letter k or K to indicate kilobytes, or m or M to indicate megabytes. The default value is chosen at runtime based on system configuration.

Packaging Your Spring Boot Project

You can use the plugin spring-boot-maven-plugin within the build section of your POM file to enable proper packaging for your Spring Boot project. This will package the project into a jar file, which can later be deployed easily on a server.

To do this, you need to include the section:

1
2
3
4
5
6
7
8
9
<!-- Assuming that you have already included the parent pom setting, which specifies the default plugin versions for you -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

Now, you can just run the command mvn package (or the equivalent in IntelliJ or Eclipse) to package your Spring Boot project.

Finally, to run your packagejar file, you can use java -jar <your-packaged-jar-file>.

Note:

  • When running your Spring Boot application, notice that it used TomCat for configuring your server. This is done by Spring Boot even if we did not specify it to do so. Therefore, by packaging via the steps mentioned above, those TomCat and other necessary dependencies will be automatically packaged in your META_INF/lib folder within your jar file.

Developer Tools

Spring Boot includes an additional set of tools that can make the application development experience a little more pleasant. The spring-boot-devtools module can be included in any project to provide additional development-time features.

For example, if you have worked on latest UI development frameworks e.g. Node, angular, gulp etc. then you must have appreciated the auto-reload of UI in browser whenever there is change in some code. Its pretty useful and saves a lot of time.

Well, same feature can be utilized in spring boot applications using spring-boot-devtools dependency provided features.

Enabling spring-boot-devtools

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

where we flagged the dependency as optional because:

  • Flagging the dependency as optional in maven (as shown above) prevents devtools from being transitively applied to other modules that use your project.

Note:

  • Developer tools are automatically disabled when running a fully packaged application. If your application is launched from java -jar or if it is started from a special classloader, then it is considered a “production application”. If that does not apply to you (i.e. if you run your application from a container), consider excluding devtools or set the -Dspring.devtools.restart.enabled=false system property.

Configuring your spring-boot-devtools

(additional reference: https://howtodoinjava.com/spring-boot2/developer-tools-module-tutorial/#:~:text=The%20spring%2Dboot%2Ddevtools%20module,default%2C%20live%20reload%20is%20enabled)

To improve the performance, dev tools cache the static content/template files to serve them faster to browser/client. This is very good feature in production where every milli-second performance improvement matters. But in development environment, it can be a problem and cause stale cache problem and you may not see your changes immediatly in browser. For this reason, springboot-devtools disables the caching options by default.

Also, because you need more information about web requests while developing Spring MVC and Spring WebFlux applications, developer tools will enable DEBUG logging for the web logging group. This will give you information about the incoming request, which handler is processing it, the response outcome, etc.

More importantly, you can also configure the springboot-devtools to meet your own needs:

  1. You can enable the static caching to use in production environment by setting a property:

    1
    2
    3
    4
    5
    6
    7
    8
    # inside application.properties
    spring.freemarker.cache = true # false by default in production environment

    # Other such properties

    # spring.thymeleaf.cache = false
    # spring.mustache.cache = false
    # spring.groovy.template.cache = false
  2. If you wish to log all request details (including potentially sensitive information), you can turn on the spring.mvc.log-request-details or spring.codec.log-request-details configuration properties.

    1
    2
    # inside application.properties
    spring.mvn.log-request-details = true # or spring.codec.log-request-details = true
  3. The spring-boot-devtools module includes an embedded LiveReload server that can be used to trigger a browser refresh when a resource is changed. Precondition is that your browser should have supported extention for it. You can find such browser extentions in this link.

    By default, live reload is enabled. If you wish to disable this feature for some reason, then set sping.devtools.livereload.enabled property to false.

    1
    2
    #inside application.properties
    spring.devtools.livereload.enabled = false # Set false to disable live reload
  4. For a complete list of the properties that are applied by the devtools, see DevToolsPropertyDefaultsPostProcessor.

Automatic Reload

The automatic reload has been introduced in the section above, which triggers a browser refresh when a resource is changed. By default, it is triggered on these paths:

  • /META-INF/maven
  • /META-INF/resources
  • /resources
  • /static
  • /public
  • /templates

If you want to disable auto-reload in browser for files in few of these paths, then use spring.devtools.restart.exclude property. For example:

1
spring.devtools.restart.exclude=static/**,public/**

If you want to watch those addtional files/paths not in the default list to reload the application, use the spring.devtools.restart.additional-paths property.

1
spring.devtools.restart.additional-paths=script/**

Similarily, If you want to exclude those additional paths, use the spring.devtools.restart.additional-exclude property.

1
spring.devtools.restart.additional-exclude=styles/**

Automatic Restart

Auto-restart means reloading the java classes and consiguration at server side. After the server side changes are re-deployed dynamically, server restart happen and load the modified code and configutation.

This can be a useful feature when working in an IDE, as it gives a very fast feedback loop for code changes. By default, any entry on the classpath that points to a directory is monitored for changes. Note that certain resources, such as static assets and view templates, do not need to restart the application (these would be more appropriate for auto-reload).

As DevTools monitors classpath resources, the only way to trigger a restart is to update the classpath. The way in which you cause the classpath to be updated depends on the IDE that you are using. In Eclipse, saving a modified file causes the classpath to be updated and triggers a restart. In IntelliJ IDEA, building the project (Build → Build Project) triggers the restart.

In general, automatic restart works very well when used with LiveReload. See the LiveReload section for details. If you use JRebel, automatic restarts are disabled in favor of dynamic class reloading. Other devtools features (such as LiveReload and property overrides) can still be used.

Note:

  • DevTools relies on the application context’s shutdown hook to close it during a restart. It does not work correctly if you have disabled the shutdown hook (SpringApplication.setRegisterShutdownHook(false)).

To enable/disable automatic restart, you could use the property spring.devtools.restart.enabled:

1
2
# inside application.properties
spring.devtools.restart.enabled = false # disables restart. Set to true to enable it

To enable/disable logging of auto-configuration changes, you need to use pring.devtools.restart.log-condition-evaluation-delta. By default, each time your application restarts, a report showing the condition evaluation delta is logged. The report shows the changes to your application’s auto-configuration as you make changes such as adding or removing beans and setting configuration properties.

To disable the logging of the report, set the following property:

1
2
# inside application.properties
spring.devtools.restart.log-condition-evaluation-delta = false # set to true to enable it

Doing so still initializes the restart classloader, but it does not watch for file changes.

If you need to completely disable restart support (for example, because it does not work with a specific library), you need to set the spring.devtools.restart.enabled System property to false before calling SpringApplication.run(…), as shown in the following example:

1
2
3
4
public static void main(String[] args) {
System.setProperty("spring.devtools.restart.enabled", "false");
SpringApplication.run(MyApp.class, args);
}

To trigger a restart with a Trigger File, you need to set the property spring.devtools.restart.trigger-file. This is useful if automatic restarts may not be desirable on every file change and sometimes can slower down development time due to frequent restarts. To solve this problem, you can use a trigger file. Spring boot will keep monitoring that file and once it will detect any modification in that file, it will restart the server and reload all your previous changes.

To use a trigger file, set the spring.devtools.restart.trigger-file property to the name (excluding
any path) of your trigger file
. The trigger file must appear somewhere on your classpath.

For example, if you have a project with the following structure:

1
2
3
4
src
+- main
+- resources
+- .reloadtrigger

Then your trigger-file property would be:

1
spring.devtools.restart.trigger-file=.reloadtrigger

Restarts will now only happen when the src/main/resources/.reloadtrigger is updated.

Some IDEs have features that save you from needing to update your trigger file manually. For IntelliJ, you can follow the instructions in their documentation.

Note:

  • You might want to set spring.devtools.restart.trigger-file as a global setting (adding a spring-boot-devtools.properties file to $HOME/.config/spring-boot directory, for example), so that all your projects behave in the same way.

Auto-Reload vs Auto-Restart

Auto-reload (or auto-refresh) refer to UI reload at browser to see static content changes. Auto-restart is referred to reloading the server side code and configurations followed by server restart.

The restart technology provided by Spring Boot works by using two classloaders. Classes that do not change (for example, those from third-party jars) are loaded into a base classloader. Classes that you are actively developing are loaded into a restart classloader. When the application is restarted, the restart classloader is thrown away and a new one is created. This approach means that application restarts are typically much faster than “cold starts”, since the base classloader is already available and populated.

If you find that** restarts are not quick enough for your applications or you encounter classloading issues, you **could consider reloading technologies such as JRebel from ZeroTurnaround. These work by rewriting classes as they are loaded to make them more amenable to reloading.

Customizing the Restart Classloader

By default, any open project in your IDE is loaded with the “restart” classloader, and any regular .jar file is loaded with the “base” classloader. If you work on a multi-module project, and not every module is imported into your IDE, you may need to customize things. To do so, you can create a META-INF/spring-devtools.properties file.

The spring-devtools.properties file can contain properties prefixed with restart.exclude and restart.include. The include elements are items that should be** pulled up into the “restart” classloader, and the **exclude elements are items that should be pushed down into the “base” classloader. The value of the property is a regex pattern that is applied to the classpath, as shown in the following example:

1
2
restart.exclude.companycommonlibs=/mycorp-common-[\\w\\d-\.]+\.jar
restart.include.projectcommon=/mycorp-myproj-[\\w\\d-\.]+\.jar

Note:

  • Restart functionality does not work well with objects that are deserialized by using a standard ObjectInputStream. If you need to deserialize data, you may need to use Spring’s ConfigurableObjectInputStream in combination with Thread.currentThread().getContextClassLoader().

LiveReload

The spring-boot-devtools module includes an embedded LiveReload server that can be used to trigger a browser refresh when a resource is changed. LiveReload browser extensions are freely available for Chrome, Firefox and Safari from livereload.com.

If you do not want to start the LiveReload server when your application runs, you can set the
spring.devtools.livereload.enabled property to false.

However, note that you can only run one LiveReload server at a time. Before starting your application, ensure that no other LiveReload servers are running. If you start multiple applications from your IDE, only the first has LiveReload support.

Note:

Configure your File System Watcher

If you find that there are times when certain changes are not reflected when devtools restarts the application, you might need to configure your File System Watcher.

FileSystemWatcher works by polling the class changes with a certain time interval, and then waiting for a predefined quiet period to make sure there are no more changes. So if you find that certain changes are not reflected, try increasing the spring.devtools.restart.poll-interval and spring.devtools.restart.quiet-period parameters to the values that fit your development environment:

1
2
spring.devtools.restart.poll-interval=2s
spring.devtools.restart.quiet-period=1s

The monitored classpath directories are now polled every 2 seconds for changes, and a 1 second quiet period is maintained to make sure there are no additional class changes.

Global Settings File

You can configure global devtools settings by adding any of the following files to the $HOME/.config/spring-boot directory:

  1. spring-boot-devtools.properties
  2. spring-boot-devtools.yaml
  3. spring-boot-devtools.yml

Any properties added to these file apply to all Spring Boot applications on your machine that use devtools. For example, to configure restart to always use a trigger file, you would add the following property:

1
2
# inside ~/.config/spring-boot/spring-boot-devtools.properties 
spring.devtools.restart.trigger-file=.reloadtrigger

Remote Applications

The Spring Boot developer tools are not limited to local development. You can also use several features when running applications remotely. Remote support is opt-in as enabling it can be a security risk. It should only be enabled when running on a trusted network or when secured with SSL.

To enable it, you need to make sure that devtools is included in the repackaged archive, as shown in
the following listing:

1
2
3
4
5
6
7
8
9
10
11
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludeDevtools>false</excludeDevtools>
</configuration>
</plugin>
</plugins>
</build>

Then you need to set the spring.devtools.remote.secret property. Like any important password or secret, the value should be unique and strong such that it cannot be guessed or brute-forced.

Then, to run the remote application within your IDE, you need to run org.springframework.boot.devtools.RemoteSpringApplication with the same classpath as the remote project that you connect to. The application’s single required argument is the remote URL to which it connects.

For example, if you are using Eclipse or STS and you have a project named my-app that you have deployed to Cloud Foundry, you would do the following:

  • Select Run Configurations… from the Run menu.
  • Create a new Java Application “launch configuration”.
  • Browse for the my-app project.
  • Use org.springframework.boot.devtools.RemoteSpringApplication as the main class.
  • Add https://myapp.cfapps.io to the Program arguments (or whatever your remote URL is).

Note:

  • Because the remote client is using the same classpath as the real application it can directly read application properties. This is how the** spring.devtools.remote.secret property is read and passed to the server for authentication**.
  • It is always advisable to use https:// as the connection protocol, so that traffic is encrypted and passwords cannot be intercepted.

Updates in your Remote Application

The remote client monitors your application classpath for changes in the same way as the local restart. Any updated resource is pushed to the remote application and (if required) triggers a restart. This can be helpful if you iterate on a feature that uses a cloud service that you do not have locally. Generally, remote updates and restarts are much quicker than a full rebuild and deploy cycle.

On a slower development environment, it may happen that the quiet period is not enough, and the changes in the classes may be split into batches. The server is restarted after the first batch of class changes is uploaded. The next batch can’t be sent to the application, since the server is restarting. This is typically manifested by a warning in the RemoteSpringApplication logs about failing to upload some of the classes, and a consequent retry. But it may also lead to application code inconsistency and failure to restart after the first batch of changes is uploaded. If you observe such problems constantly, try increasing the spring.devtools.restart.poll-interval and spring.devtools.restart.quiet-period parameters to the values that fit your development environment.

Note:

  • Files are only monitored when the remote client is running. If you change a file before starting the remote client, it is not pushed to the remote server.

Spring Boot Features


The SpringApplication class provides a convenient way to bootstrap a Spring application that is started from a main() method. In many situations, you can delegate to the static SpringApplication.run method, as shown in the following example:

1
2
3
public static void main(String[] args) {
SpringApplication.run(MySpringConfiguration.class, args);
}

When your application starts, you should see something similar to the following output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  .   ____          _            __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.3.0.RELEASE)

2020-06-16 15:35:35.216 INFO 47936 --- [ restartedMain] App : Starting App on XY-Laptop with PID 47936 (E:\Maven Workspace\testSpringBoot\target\classes started by 26238 in E:\Maven Workspace\testSpringBoot)
2020-06-16 15:35:35.218 INFO 47936 --- [ restartedMain] App : No active profile set, falling back to default profiles: default
2020-06-16 15:35:35.248 INFO 47936 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2020-06-16 15:35:35.248 INFO 47936 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2020-06-16 15:35:36.345 INFO 47936 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2020-06-16 15:35:36.351 INFO 47936 --- [ restartedMain] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2020-06-16 15:35:36.351 INFO 47936 --- [ restartedMain] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.35]
2020-06-16 15:35:36.399 INFO 47936 --- [ restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2020-06-16 15:35:36.399 INFO 47936 --- [ restartedMain] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1151 ms
2020-06-16 15:35:36.504 INFO 47936 --- [ restartedMain] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-06-16 15:35:36.588 INFO 47936 --- [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729
2020-06-16 15:35:36.623 INFO 47936 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2020-06-16 15:35:36.630 INFO 47936 --- [ restartedMain] App : Started App in 1.652 seconds (JVM running for 1.947)

Startup Failures

If your application fails to start, registered FailureAnalyzers get a chance to provide a dedicated error message and a concrete action to fix the problem. For instance, if you start a web application on port 8080 and that port is already in use, you should see something similar to the following message:

1
2
3
4
5
6
7
8
***************************
APPLICATION FAILED TO START
***************************
Description:
Embedded servlet container failed to start. Port 8080 was already in use.
Action:
Identify and stop the process that's listening on port 8080 or configure this
application to listen on another port.

If no failure analyzers are able to handle the exception, you can still display the full conditions report to better understand what went wrong. To do so, you need to enable the debug property or enable DEBUG logging for org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener.

For instance, if you are running your application by using java -jar, you can enable the debug
property as follows:

1
$ java -jar myproject-0.0.1-SNAPSHOT.jar --debug

Lazy Initialization

SpringApplication allows an application to be initialized lazily (similar to that of the Spring Framework). When lazy initialization is enabled, beans are created as they are needed rather than during application startup. As a result, enabling lazy initialization can reduce the time that it takes your application to start. In a web application, enabling lazy initialization will result in many web-related beans not being initialized until an HTTP request is received.

A downside of lazy initialization is that it can delay the discovery of a problem with the application. If a misconfigured bean is initialized lazily, a failure will no longer occur during startup and the problem will only become apparent when the bean is initialized. Care must also be taken to ensure that the JVM has sufficient memory to accommodate all of the application’s beans and not just those that are initialized during startup.

To enable lazy initialization, you can use:

  • the lazyInitialization method on SpringApplicationBuilder
  • the setLazyInitialization method on SpringApplication
  • it can be enabled using the spring.main.lazy-initialization property, for example: spring.main.lazy-initialization=true
  • or you can set a specific bean to be lazy initialized with @Lazy(true), or at a class level with @Lazy (making all beans of that class to be lazily initialized)

Customzing your SpringApplication

If the SpringApplication defaults are** not to your taste, you can instead create a local instance** and customize it. For example, to turn off the banner (more details on customzing your banner, please visit this link), you could write:

1
2
3
4
5
public static void main(String[] args) {
SpringApplication app = new SpringApplication(MySpringConfiguration.class);
app.setBannerMode(Banner.Mode.OFF);
app.run(args);
}

It is also possible to configure the SpringApplication by using an application.properties file. See Externalized Configuration for details.

When deployed on platforms, applications can provide information about their availability to the platform using infrastructure such as Kubernetes Probes. Spring Boot includes out-of-the box support for the commonly used “liveness” and “readiness” availability states. If you are using Spring Boot’s “actuator” support then these states are exposed as health endpoint groups.

In addition, you can also obtain availability states by injecting the ApplicationAvailability interface into your own beans.

Application Liveness

Application Readiness

Application Event and Listeners

Web Applications

Accessing Application Arguements

If you need to access the application arguments that were passed to SpringApplication.run(…), you can inject a org.springframework.boot.ApplicationArguments bean. The ApplicationArguments interface provides access to both the raw String[] arguments as well as parsed option and non-option arguments, as shown in the following example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.boot.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.stereotype.*;

@Component
public class MyBean {

@Autowired // bean injected here
public MyBean(ApplicationArguments args) {
boolean debug = args.containsOption("debug");
List<String> files = args.getNonOptionArgs();
// if run with "--debug logfile.txt" debug=true, files=["logfile.txt"]
}
}

Running Specific Code for SpringApplication

If you need to run some specific code once the SpringApplication has started, you can implement the ApplicationRunner or CommandLineRunner interfaces. Both interfaces work in the same way and offer a single run method, which is called just before SpringApplication.run(…) completes.

The CommandLineRunner interfaces provides access to application arguments as a simple string array, whereas the ApplicationRunner uses the ApplicationArguments interface discussed earlier. The following example shows a CommandLineRunner with a run method:

1
2
3
4
5
6
7
8
9
import org.springframework.boot.*;
import org.springframework.stereotype.*;
@Component
public class MyBean implements CommandLineRunner {

public void run(String... args) {
// Do something...
}
}

If several CommandLineRunner or ApplicationRunner beans are defined that must be called in a specific order, you can additionally implement the org.springframework.core.Ordered interface or use the org.springframework.core.annotation.Order annotation (e.g. @Order(1), remember that the smaller the number, the higher the priority).

Applicaiton Exit

Each SpringApplication registers a shutdown hook with the JVM to ensure that the ApplicationContext closes gracefully on exit. All the standard Spring lifecycle callbacks (such as the DisposableBean interface or the @PreDestroy annotation) can be used.

In addition, beans may implement the org.springframework.boot.ExitCodeGenerator interface if they wish to return a specific exit code when SpringApplication.exit() is called. This exit code can then be passed to System.exit() to return it as a status code, as shown in the following example:

1
2
3
4
5
6
7
8
9
10
11
@SpringBootApplication
public class ExitCodeApplication {
@Bean
public ExitCodeGenerator exitCodeGenerator() {
return () -> 42;
}

public static void main(String[] args) {
System.exit(SpringApplication.exit(SpringApplication.run(ExitCodeApplication.class,args)));
}
}

Also, the ExitCodeGenerator interface may be implemented by exceptions. When such an exception is encountered, Spring Boot returns the exit code provided by the implemented getExitCode() method.

Externalized Configuration

Spring Boot lets you externalize your configuration so that you can work with the same application code in different environments. You can use properties files, YAML files, environment variables, and command-line arguments to externalize configuration.

Property values can be injected directly into your beans by using the @Value annotation, accessed through Spring’s Environment abstraction, or be bound to structured objects through @ConfigurationProperties. Spring Boot uses a very particular PropertySource order that is designed to allow sensible overriding of values. Properties are considered in the following order:

  1. Devtools global settings properties in the $HOME/.config/spring-boot directory when devtools is active.
  2. @TestPropertySource annotations on your tests.
  3. properties attribute on your tests. Available on @SpringBootTest and the test annotations for testing a particular slice of your application.
  4. Command line arguments.
  5. Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property).
  6. ServletConfig init parameters.
  7. ServletContext init parameters.
  8. JNDI attributes from java:comp/env.
  9. Java System properties (System.getProperties()).
  10. OS environment variables.
  11. A RandomValuePropertySource that has properties only in random.*.
  12. Profile-specific application properties outside of your packaged jar (application-{profile}.properties and YAML variants).
  13. Profile-specific application properties packaged inside your jar (application-{profile}.properties and YAML variants).
  14. Application properties outside of your packaged jar (application.properties and YAML variants).
  15. Application properties packaged inside your jar (application.properties and YAML variants).
  16. @PropertySource annotations on your @Configuration classes. Please note that such property sources are not added to the Environment until the application context is being refreshed. This is too late to configure certain properties such as logging.* and spring.main.* which are read before refresh begins.
  17. Default properties (specified by setting SpringApplication.setDefaultProperties).

To provide a concrete example, suppose you develop a @Component that uses a name property, as shown in the following example:

1
2
3
4
5
6
7
8
import org.springframework.stereotype.*;
import org.springframework.beans.factory.annotation.*;
@Component
public class MyBean {
@Value("${name}")
private String name;
// ...
}

On your application classpath (for example, inside your jar) you can have an application.properties file that provides a sensible default property value for name. When running in a new environment, an application.properties file can be provided outside of your jar that overrides the name. For one-off testing, you can launch with a specific command line switch (for example,java -jar app.jar --name="Spring").

Using a Wildcard for Loading Configuration Files

Spring Boot also supports wildcard locations when loading configuration files. By default, a wildcard location of config/*/ outside of your jar is supported. Wildcard locations are also supported when specifying spring.config.additional-location and spring.config.location.

Wildcard locations are particularly useful in an environment such as Kubernetes when there are multiple sources of config properties. For example, if you have some Redis configuration and some MySQL configuration, you might want to keep those two pieces of configuration separate, while requiring that both those are present in an application.properties that the app can bind to. This might result in two separate application.properties files mounted at different locations such as /config/redis/application.properties and /config/mysql/application.properties. In such a case, having a wildcard location of config/*/, will result in both files being processed.

Note:

  • A wildcard location must contain only one and end with / for search locations that are directories or */<filename> for search locations that are files. Locations with wildcards are sorted alphabetically based on the absolute path of the file names.

Generating and Using Random Values

The RandomValuePropertySource is useful for injecting random values (for example, into secrets or test cases). It can produce integers, longs, uuids, or strings, as shown in the following example

1
2
3
4
5
6
my.secret=${random.value}
my.number=${random.int}
my.bignumber=${random.long}
my.uuid=${random.uuid}
my.number.less.than.ten=${random.int(10)}
my.number.in.range=${random.int[1024,65536]}

The random.int* syntax is OPEN value (,max) CLOSE where the OPEN,CLOSE are any character and value,max are integers. If max is provided, then value is the minimum value and max is the maximum value (exclusive).

Command Line Properties

By default, SpringApplication converts any command line option arguments (that is, arguments starting with --, such as --server.port=9000) to a property and adds them to the Spring Environment. As mentioned previously, command line properties always take precedence over other property sources.

If you do not want command line properties to be added to the Environment, you can disable them by using SpringApplication.setAddCommandLineProperties(false).

Application Properties File

SpringApplication loads properties from application.properties files in the following locations and adds them to the Spring Environment:

  1. A /config subdirectory of the current directory
  2. The current directory
  3. A classpath /config package
  4. The classpath root

The list is ordered by precedence (properties defined in locations higher in the list override those
defined in lower locations)
.

If you do not like application.properties as the configuration file name, you can switch to another file name by specifying a spring.config.name environment property. You can also refer to an explicit location by using the spring.config.location environment property (which is a comma-separated list of directory locations or file paths). The following example shows how to specify a different file name:

1
$ java -jar myproject.jar --spring.config.name=myproject

The following example shows how to specify two locations:

1
2
$ java -jar myproject.jar
--spring.config.location=classpath:/default.properties,classpath:/override.properties

However, if you used a directory for searching that application.properties, so that you specify custom config locations by using spring.config.location, they replace the default search locations.

For example, if spring.config.location is configured with the value classpath:/custom-config/,file:./custom-config/, the search order becomes the following:

  1. file:./custom-config/
  2. classpath:custom-config/

Instead of/Overrriding the default locations (file:./config/ is searched first):

  1. file:./config/
  2. file:./config/*/
  3. file:./
  4. classpath:/config/
  5. classpath:/

To prevent the overriding behavior, use spring.config.additionallocation, so that they are used in addition to the default locations. Additional locations are searched before the default locations. For example, if additional locations of classpath:/customconfig/, file:./custom-config/ are configured, the search order becomes the following:

  1. file:./custom-config/
  2. classpath:custom-config/
  3. file:./config/
  4. file:./config/*/
  5. file:./
  6. classpath:/config/
  7. classpath:/

Profile Specific Properties

Using .properties file

In addition to application.properties files, profile-specific properties can also be defined by using the following naming convention: application-{profile}.properties

Profile-specific properties are loaded from the same locations as standard application.properties, with profile-specific files always overriding the non-specific ones, whether or not the profile specific files are inside or outside your packaged jar.

If several profiles are specified, a last-wins strategy applies. For example, profiles specified by the spring.profiles.active property are added after those configured through the SpringApplication API and therefore take precedence.

If that didn’t work, you can use either command line options or the SpringApplication.setAdditionalProfiles() option (if both are specified, the command line option takes precedence):

  • Command line option:

  • add the option: -Dspring.profiles.active=dev or -Dspring.profiles.active=prod to enable the usage of the application-dev.properties profile or the application-prod.properties profile, respectively

  • SpringApplication

    • before calling SpringApplication.run() function, do:

      1
      2
      3
      4
      5
      public static void main(String[] args) {
      SpringApplication app = new SpringApplication(App.class);
      app.setAdditionalProfiles("dev");
      app.run(args);
      }

      to turn on the application-dev.properties. Switch to ("prod") to enable the application-prod.properties.

Using .yml file

If you use .yml file, you can define different application profile setting within the same file, separated with ---. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# inside application.yml
spring:
profiles:
active: dev

server:
port: 8000

logging:
level:
root: off

---
spring:
profiles: dev

server:
port: 8888

---

spring:
profiles: prod

server:
port: 80

An additional benefit of this is that you can use any name you want for specifying the profile, since they are in the same file. Therefore, you can do something like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# inside application.yml
spring:
profiles:
active: cust

server:
port: 8000

logging:
level:
root: off

---
spring:
profiles: cust

server:
port: 8888

Notice:

  • You now need to specify which profile each segment refers to. For example, dev profile needs to be specified with spring: profiles: dev. The segment not specifying any spring: profiles argument is seen as the default application profile.
  • Also, to enable a profile, you need to specify spring: profiles: active: <profile-name> within the default application profile. This is the same as the process in application.properties.

Injecting Property Values

Basically, once the application.properties or other customized application.properties has been loaded, you can inject values with the placeholder ${}.

In the main application where you run your SpringApplication, you can inject them easily with @Value("${<some-expression>}"). For example:

1
2
# inside application.properties
app.my.name=jason

then in your main application:

1
2
@Value("${app.my.name}")
private String appName;

However, in some other places, besides the main application, you might need to specify your application.properties file again before injecting the @Value. For example, in a @Configuration file at some other place in your project hierarchy:

1
2
3
4
5
6
7
8
9
@Configuration
@PropertySource("classpath:application.properties")
public class MainConfig {
@Bean
public ProfileManager profileManager(@Autowired Environment env, @Value("${app.testname}") String s) {
return new ProfileManager(env,s);
}

}

Note:

  • Here constructor injection is used. For reason why constructor injection is preferred over field injection, please visit the Spring Framework Guide.

You can also use this to calculate expressions, which will require #{} instead of ${} for @Value(). For example:

1
2
@Value(#{app.my.age*2})
private int age;

Type-safe Configuration Properties

JavaBean properties binding with application.properties

It is possible to bind a bean declaring standard JavaBean properties as shown in the following example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.example;

import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("acme") // matches the all the properties starting with `acme`
public class AcmeProperties {
private boolean enabled; // matches acme.enabled
private InetAddress remoteAddress; // matches acme.remote-address
private final Security security = new Security(); // matches acme.security.*

public boolean isEnabled() { ... }
public void setEnabled(boolean enabled) { ... }
public InetAddress getRemoteAddress() { ... }
public void setRemoteAddress(InetAddress remoteAddress) { ... }
public Security getSecurity() { ... }

public static class Security {
private String username; // matches acme.security.username
private String password; // matches acme.security.password
private List<String> roles = new ArrayList<>(Collections.singleton("USER"));

public String getUsername() { ... }
public void setUsername(String username) { ... } // notice the setters here
public String getPassword() { ... }
public void setPassword(String password) { ... } // notice the setters here
public List<String> getRoles() { ... }
public void setRoles(List<String> roles) { ... }
}
}
Constructor Binding

The previous example could be made immutable with constructor binding and making them final:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.example;

import java.net.InetAddress;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.bind.DefaultValue;

@ConstructorBinding // enables constructor binding, so it looks for constructors for parameters to be injected
@ConfigurationProperties("acme")
public class AcmeProperties {
private final boolean enabled; // notice the final keyword
private final InetAddress remoteAddress;
private final Security security;

public AcmeProperties(boolean enabled, InetAddress remoteAddress, Security
security) { // this woud be required for constructor binding
this.enabled = enabled;
this.remoteAddress = remoteAddress;
this.security = security;
}
public boolean isEnabled() { ... }
public InetAddress getRemoteAddress() { ... }
public Security getSecurity() { ... }

public static class Security {
private final String username;
private final String password;
private final List<String> roles;
public Security(String username, String password,
@DefaultValue("USER") List<String> roles) { // notice the constructor here as well
this.username = username;
this.password = password;
this.roles = roles;
}
public String getUsername() { ... }
public String getPassword() { ... }
public List<String> getRoles() { ... }
}
}

In this setup, the @ConstructorBinding annotation is used to indicate that constructor binding should be used. This means that the binder will expect to find a constructor with the parameters that you wish to have bound (Nested members of a @ConstructorBinding class (such as Security in the example above) will also be bound via their constructor).

Note:

  • To use constructor binding the class must be enabled using @EnableConfigurationProperties or configuration property scanning. You cannot use constructor binding with beans that are created by the regular Spring mechanisms (e.g. @Component beans, beans created via @Bean methods or beans loaded using @Import).
Enabling @ConfigurationProperties

Spring Boot provides infrastructure to bind @ConfigurationProperties types and register them as beans. You can either enable configuration properties on a class-by-class basis or** enable configuration property scanning** that works in a similar manner to component scanning.

Sometimes, classes annotated with @ConfigurationProperties might not be suitable for scanning, for example, if you’re developing your own auto-configuration or you want to enable them conditionally. In these cases, specify the list of types to process using the @EnableConfigurationProperties annotation. This can be done on any @Configuration class, as shown in the following example:

1
2
3
4
5
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(AcmeProperties.class) // this will be not necessary if
// you use @ConfigurationPropertiesScane below
public class MyConfiguration {
}

To use configuration property scanning, add the @ConfigurationPropertiesScan annotation to your application. Typically, it is added to the main application class that is annotated with @SpringBootApplication but it can be added to any @Configuration class.

By default, scanning will occur from the package of the class that declares the annotation. If you want to define specific packages to scan, you can do so as shown in the following example:

1
2
3
4
@SpringBootApplication
@ConfigurationPropertiesScan({ "com.example.app", "org.acme.another" })
public class MyApplication {
}

One important thing to note is that:

  • When the @ConfigurationProperties bean is registered using configuration property scanning or via @EnableConfigurationProperties, the bean has a conventional name: <prefix>-<fqn>, where <prefix> is the environment key prefix specified in the @ConfigurationProperties annotation and <fqn> is the fully qualified name of the bean. If the annotation does not provide any prefix, only the fully qualified name of the bean is used.

    The bean name in the example above is acme-com.example.AcmeProperties. This means you can do

    1
    2
    @Resource(name = "acme-com.example.AcmeProperties")		// instead of @Autowire, if you prefer
    private AcmeProperties acmeProperties;

In general, it is not recommended to inject other beans while using property binding. However, if you still want to inject other beans using the constructor, the configuration properties bean must be annotated with @Component and use JavaBean-based property binding.

Properties Binding to a Bean

To configure a bean from the Environment properties, add @ConfigurationProperties to its bean registration, as shown in the following example:

1
2
3
4
5
@ConfigurationProperties(prefix = "another")
@Bean
public AnotherComponent anotherComponent(...) {
...
}

Any JavaBean property defined with the another prefix is mapped onto that AnotherComponent bean in manner similar to the preceding AcmeProperties example.

Binding With application.yml

Binding Objects/Maps/Lists

When binding to Map properties, if the key contains anything other than lowercase alpha-numeric characters or -, you need to use the bracket notation so that the original value is preserved. If the key is not surrounded by [], any characters that are not alpha-numeric or - are removed. For example, consider binding the following properties to a Map:

1
2
3
4
5
acme:
map:
"[/key1]": value1
"[/key2]": value2
/key3: value3

The properties above will bind to a Map with /key1, /key2 and key3 as the keys in the map. (For YAML files, the brackets need to be surrounded by quotes for the keys to be parsed properly.)

The above can be mapped to acme.map./key1 , but can also be used to represent fields in an Object:

1
2
3
4
5
6
person1:
lastName: john
firstName: cole
age: 10
pets: [pp, cc]
father: {lastName: john, firstName: ccole, age: 33, pets: []}

This could then be mapped to:

1
2
3
4
5
6
7
8
9
10
@ConfigurationProperties("person1")	// this is needed so that the correct data is mapped
public class Person{
private String lastName;
private String firstName;
private int age;
private List<String> pets;
private Person father;

... // if have a constructor here, you can also use constructor injection
}

Note:

  • There are two ways to specify a map/object withkey:value pairs. You can either specify them with a new line and some spacing, or you can specify them in one line with the {} bracket, separated with , comma.
  • There are also two ways to specify a list object. You can either specify them with a new line and a - for each new object, or, in one line, with [] bracket, each separated with a , comma.

Binding with a Customized yml or properties file

You can also also specify a customized properties file for constructor injection or property bindings. The basic annotations and syntax are the same as above, except that you need to specify that customized yml or properties file with @PropertySource(). For example:

1
2
3
4
@PropertySource(value="classpath: customizedPropertyFile")
@ConfigurationProperties(prefix="person1")
public class Person
...

Relaxed Name Matching and Binding for Environmental Variables

Spring Boot uses some relaxed rules for binding Environment properties to @ConfigurationProperties beans, so there does not need to be an exact match between the Environment property name and the bean property name. Common examples where this is useful include dash-separated environment properties (for example, context-path binds to contextPath), and capitalized environment properties (for example, PORT binds to port).

Spring Boot’s relaxed binding rules are, as much as possible, designed to be compatible with naming estrictions in different operating systems, such as Unix and Linux.

To convert a property name in the canonical-form to an environment variable name you can follow
these rules:

  • Replace dots (.) with underscores (_).
  • Remove any dashes (-).
  • Convert to uppercase.

For example, the configuration property spring.main.log-startup-info would be an environment variable named SPRING_MAIN_LOGSTARTUPINFO.

Properties Type Conversion

Spring Boot attempts to coerce the external application properties to the right type when it binds to the @ConfigurationProperties beans.

If you need custom type conversion, you can provide a ConversionService bean (with a bean named conversionService) or custom property editors (through a CustomEditorConfigurer bean) or custom Converters (with bean definitions annotated as @ConfigurationPropertiesBinding).

Converting time durations

Spring Boot has dedicated support for expressing durations. If you expose a java.time.Duration property, the following formats in application properties are available:

  • A regular long representation (using milliseconds as the default unit unless a @DurationUnit has been specified)
  • The standard ISO-8601 format used by java.time.Duration
  • A more readable format where the value and the unit are coupled (e.g. 10s means 10 seconds)

Consider the following example:

1
2
3
# inside your application,properties, suppose those units are in seconds
app.time.sessionTimeout=240
app.time.readTimeout=120
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@ConfigurationProperties("app.system")
@PropertySource("classpath:application.properties")
public class AppSystemProperties {
@DurationUnit(ChronoUnit.SECONDS)
private Duration sessionTimeout;
@DurationUnit(ChronoUnit.SECONDS)
private Duration readTimeout;

public Duration getSessionTimeout() {
return this.sessionTimeout;
}
public void setSessionTimeout(Duration sessionTimeout) {
this.sessionTimeout = sessionTimeout;
}
public Duration getReadTimeout() {
return this.readTimeout;
}
public void setReadTimeout(Duration readTimeout) {
this.readTimeout = readTimeout;
}
}

If you specified a session timeout of 30 seconds in application.properties, getting 30, PT30S and 30s are all equivalent, if you specified @DurationUnit(ChronoUnit.SECONDS). A read timeout of 500ms can be specified in any of the following form: 500, PT0.5S and 500ms, without specifying the @DurationUnit.

Alternatively, you can use constructor binding:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@ConstructorBinding
@ConfigurationProperties("app.time")
@PropertySource("classpath:application.properties")
public class TimeProperties {
@DurationUnit(ChronoUnit.SECONDS)
private final long sessionTimeout;
@DurationUnit(ChronoUnit.SECONDS)
private final Duration readTimeout;

public TimeProperties(long sessionTimeout, Duration readTimeout){
this.sessionTimeout = sessionTimeout;
this.readTimeout = readTimeout;
}

public long getSessionTimeout() {
return this.sessionTimeout;
}

public Duration getReadTimeout() {
return this.readTimeout;
}
}

Converting Periods

In addition to durations, Spring Boot can also work with java.time.Period type. This is essentially the same as Duration shown above.

The following formats can be used in application properties:

  • An regular int representation (using days as the default unit unless a @PeriodUnit has been specified)
  • The standard ISO-8601 format used by java.time.Period
  • A simpler format where the value and the unit pairs are coupled (e.g. 1y3d means 1 year and 3 days)

Converting Data Size

Spring Framework has a DataSize value type that expresses a size in bytes. If you expose a DataSize
property, the following formats in application properties are available:

  • A regular long representation (using bytes as the default unit unless a @DataSizeUnit has been specified)
  • A more readable format where the value and the unit are coupled (e.g. 10MB means 10 megabytes)

Consider the following example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@ConfigurationProperties("app.io")
public class AppIoProperties {
@DataSizeUnit(DataUnit.MEGABYTES)
private DataSize bufferSize = DataSize.ofMegabytes(2);
private DataSize sizeThreshold = DataSize.ofBytes(512);

public DataSize getBufferSize() {
return this.bufferSize;
}
public void setBufferSize(DataSize bufferSize) {
this.bufferSize = bufferSize;
}
public DataSize getSizeThreshold() {
return this.sizeThreshold;
}
public void setSizeThreshold(DataSize sizeThreshold) {
this.sizeThreshold = sizeThreshold;
}
}

To specify a buffer size of 10 megabytes, 10 and 10MB are equivalent. A size threshold of 256 bytes can be specified as 256 or 256B.

You can also use any of the supported units. These are:

  • B for bytes
  • KB for kilobytes
  • MB for megabytes
  • GB for gigabytes
  • TB for terabytes

The default unit is bytes and can be overridden using @DataSizeUnit as illustrated in the sample above.

Validating @ConfigurationProperties

Spring Boot attempts to validate@ConfigurationProperties classes whenever they are annotated with Spring’s @Validated annotation. You can use JSR-303 javax.validation constraint annotations directly on your configuration class. To do so, ensure that a compliant JSR-303 implementation is on your classpath and then add constraint annotations to your fields, as shown in the following example:

1
2
3
4
5
6
7
@ConfigurationProperties(prefix="acme")
@Validated
public class AcmeProperties {
@NotNull
private InetAddress remoteAddress;
// ... getters and setters
}

This will prevent the application from running if that field annotated with @NotNull is actually null. Then, if that is the case, you will see the error in console:

1
2
3
Property: app.time.writeTimeout
Value: null
Reason: cannot be null

Note:

  • To use this feature, you need to add the dependency
    1
    2
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
  • You can also trigger validation by annotating the @Bean method that creates the configuration properties with @Validated

To ensure that validation is always triggered for nested properties, the associated field must be annotated with @Valid, even when no properties are found. The following example builds on the preceding AcmeProperties example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ConfigurationProperties(prefix="acme")
@Validated
public class AcmeProperties {
@NotNull
private InetAddress remoteAddress;
@Valid // notice the @Valid here
private final Security security = new Security();
// ... getters and setters

public static class Security {
@NotEmpty
public String username;
// ... getters and setters
}
}

Logging

Spring Boot uses logging frameworks to produce logs, whether in the console or output to a file. Logging frameworks are commonly used as they can be decoupled from your code (to some extent), and would be easier to maintain and manipulate.

There are also quite some options that you can specify with logging in Spring Boot, which uses LogBack as the default logging framework implementation for SLF4J abstraction. You can change the default logging level to options such as debug or trace to see more information about how Spring Boot configured your application, logging-file-name to produce logs to a local file.

Note:

  • Since we used the LogBack implementation of the SLF4J abstraction framework for logging, we should use logging API from the SLF4J abstraction, instead of the LogBack implementation.

Using SLF4J API

You can follow the official manual on their official site. A simple example of using it would be:

1
2
3
4
5
6
7
8
9
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloWorld {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(HelloWorld.class);
logger.info("Hello World");
}
}

However, this would not be useful as we are using an abstraction level framework:

image from http://www.slf4j.org/images/concrete-bindings.png"

However, since the actual implementation is not controlled by the abstraction level, if you want to configure your logger, you need to configure based on the implementation (e.g. LogBack), not on the abstraction.

Legacy/Bridging

Q: What if I am using other products in my Spring Boot project, but those products use frameworks other than SLF4J?

For example, Spring Framework actually uses JavaCommonsLogging framework.

image from http://www.slf4j.org/images/legacy.png

Then you need to use those “Adaptation” jar packages to replace the original jar packages, which contains the same classes as in their original jar packages, but will redirect output to your SLF4J framework.

To achieve this, you need to do:

  1. Exclude (but not delete) those original jar packages from the other products you are using
  2. Include the “Adaptation” jar packages shown in the image above in those products
  3. Use the SLF4J API as usual

For instance, to exclude the commons-logging package in your Spring Framework, you need to add in your pom.xml file:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>

Spring Boot Logging

As said before, Spring Boot uses the SLF4J abstraction framework for logging. Therefore, it needs to use adaptation jar files to bridge legacy/other logger frameworks included in your projects.

If you look at the dependency diagram of your project, you can see a similar diagram as shown below:

springboot-logging.png

This means that Spring Boot has done the first two steps mentioned above for you.

Note:

  • If you are using external dependencies not directly maintained by Spring Boot, you need to manually add the section <exclusions><exclusions>in your pom.xml to exclude that logging package.

Using SLF4J Logging

In your test class, you can use the LoggerFactory to obtain your logger, then output different logging levels as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootTest
class Testsb2ApplicationTests {
Logger logger = LoggerFactory.getLogger(getClass());

@Test
void contextLoads() {
// the followings are logger levels, in ascending order
// you can use this to decide which logging should be filtered
// e.g. If you specified "info" level (the default), then info, warn, error logging will shouw
logger.trace("This is my trace log");
logger.debug("This is my debug log");
logger.info("This is my info log");
logger.warn("This is my warn log");
logger.error("This is my error log");
}

}

If you now run your application, you will see logs with info, warn and error only. This is because that Spring Boot uses the default logging level of info, meaning that only logger levels equal to and higher than info will be shown.

Log Format

The default log output from Spring Boot resembles the following example:

1
2
3
4
5
6
7
8
9
10
2020-06-19 18:33:04.627  INFO 25956 --- [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2020-06-19 18:33:04.628 INFO 25956 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2020-06-19 18:33:05.317 INFO 25956 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2020-06-19 18:33:05.322 INFO 25956 --- [ restartedMain] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2020-06-19 18:33:05.322 INFO 25956 --- [ restartedMain] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.35]
2020-06-19 18:33:05.408 INFO 25956 --- [ restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2020-06-19 18:33:05.409 INFO 25956 --- [ restartedMain] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 781 ms
2020-06-19 18:33:05.645 INFO 25956 --- [ restartedMain] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-06-19 18:33:05.758 INFO 25956 --- [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729
2020-06-19 18:33:05.791 INFO 25956 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''

The following items are output:

  • Date and Time: Millisecond precision and easily sortable.
  • Log Level: ERROR, WARN, INFO, DEBUG, or TRACE.
  • Process ID.
  • A --- separator to distinguish the start of actual log messages.
  • Thread name: Enclosed in square brackets (may be truncated for console output).
  • Logger name: This is usually the source class name (often abbreviated).
  • The log message.
debug log

By default, ERROR-level, WARN-level, and INFO-level messages are logged. You can also enable a “debug” mode by starting your application with a --debug flag, or by specifying debug=true in your application.properties.

When the debug mode is enabled, a selection of core loggers (embedded container, Hibernate, and Spring Boot)** are configured to output more information. Enabling the debug mode does **not configure your application to log all messages with DEBUG level.

trace log

Alternatively, you can enable a “trace” mode by starting your application with a --trace flag (or trace=true in your application.properties). Doing so enables trace logging for a selection of core loggers (embedded container, Hibernate schema generation, and the whole Spring portfolio).

Specify Your Own Logging Pattern

You can specify the output pattern of your loggers with the logging.pattern.console or logging.pattern.file in your application.properties or application.yml file. For example:

1
logging.pattern.file=%d{yyyy-MM-dd}[%thread] %-5level %msg%n

Configuring Your Log Levels

Configuring Log Levels for a class/specific logger

All the supported logging systems can have the logger levels set in the Spring Environment (for example, in application.properties) by using logging.level.<logger-name>=<level> where level is one of TRACE, DEBUG, INFO, WARN, ERROR, FATAL, or OFF. The root logger can be configured by using logging.level.root.

The following example shows potential logging settings in application.properties:

1
2
3
logging.level.root=debug
logging.level.org.springframework.web=info
logging.level.org.hibernate=error

Note:

  • In the previous section, it is seen that some of the loggers have the name o.s.boot or o.s.b. They actually stand for org.springframework.boot. So you can configure them by specifying the logger level of org.springframework.boot.

You can also specify logger levels for your own package (e.g. com.example):

1
logging.level.com.example=debug

Note:

  • All the other loggers that you did not specify will use the default level (root level) of info
Configuring Log Levels using Log Groups

It’s often useful to be able to group related loggers together so that they can all be configured at the same time. For example, you might commonly change the logging levels for all Tomcat related loggers, but you can’t easily remember top level packages.

To help with this, Spring Boot allows you to define logging groups in your Spring Environment. For example, here’s how you could define a “tomcat” group by adding it to your application.properties:

1
logging.group.tomcat=org.apache.catalina, org.apache.coyote, org.apache.tomcat

Once defined, you can change the level for all the loggers in the group with a single line:

1
logging.level.tomcat=TRACE

Spring Boot includes the following pre-defined logging groups that can be used out-of-the-box:

Name Loggers
web org.springframework.core.codec, org.springframework.http, org.springframework.web, org.springframework.boot.actuate.endpoint.web, org.springframework.boot.web.servlet.ServletContextInitializerBeans
sql org.springframework.jdbc.core, org.hibernate.SQL, org.jooq.tools.LoggerListener

File Output

By default, Spring Boot logs only to the console and does not write log files. If you want to write log files in addition to the console output, you need to set a logging.file.name or logging.file.path property.

For example, to specify your output logging file to ./myLog.log, where the current directory would be your project directory (where your POM file resides), you could write:

1
2
# inside application.properties
logging.file.name=./myLog.log

The following table shows how the logging.* properties can be used together:

logging.file.name logging.file.path Example Description
(none) (none) logging.file.name= Console only logging.
Specific file (none) logging.file.name=my.log Writes to the specified log file. Names can be an exact location or relative to the current directory.
(none) Specific directory logging.file.path=/var/log Writes spring.log to the specified directory. Names can be an exact location or relative to the current directory.

Log files rotate when they reach 10 MB and, as with console output, ERROR-level, WARN-level, and INFO
-level messages are logged by default. However, if you have specified debug or trace that modifies the console log output, the log written to file will also include those debug or trace information.

Besides that, there are several other defaults that Spring Boot uses but can be configured:

  • Size limits can be changed using the logging.file.max-size property (by default it is 10MB).
  • Rotated log files of the last 7 days are kept by default unless the logging.file.max-history property has been set.
  • The total size of log archives can be capped using logging.file.total-sizecap. When the total size of log archives exceeds that threshold, backups will be deleted.
  • To force log archive cleanup on application startup, use the logging.file.clean-history-on-start property.

Logback Extensions

By default, the logging system you use is Logback. You can use and configure other logging systems as well (for more details, please visit this link). But if you use Logback, Spring Boot includes a number of extensions to Logback that can help with advanced configuration.

You can use these extensions in your logback-spring.xml(recommended) /or logback.xml configuration file, which you can add under the same directory as your application.properties file

Note:

  • Because the standard logback.xml configuration file is loaded too early (directly by the logger), you cannot use extensions in it. You need to either use logback-spring.xml or define a logging.config property which would be loaded by Spring Boot.
  • The extensions cannot be used with Logback’s configuration scanning. If you attempt to do so, making changes to the configuration file results in an error.
Profile Specific Configuration

If you use logback-spring.xml instead of logback.xml, you will be able to use this additional functionality provided by Spring Boot.

The <springProfile> tag lets you optionally include or exclude sections of configuration based on the active Spring profiles. Profile sections are supported anywhere within the <configuration> element.

Use the name attribute to specify which profile accepts the configuration. The <springProfile> tag can contain a simple profile name (for example staging) or a profile expression.

  • A profile expression allows for more complicated profile logic to be expressed, for example production & (eu-central | eu-west). Check the reference guide for more details.

The following listing shows three sample profiles:

1
2
3
4
5
6
7
8
9
10
11
<springProfile name="staging">
<!-- configuration to be enabled when the "staging" profile is active -->
</springProfile>

<springProfile name="dev | staging">
<!-- configuration to be enabled when the "dev" or "staging" profiles are active -->
</springProfile>

<springProfile name="!production">
<!-- configuration to be enabled when the "production" profile is not active -->
</springProfile>
Environment Properties

The <springProperty> tag lets you expose properties from the Spring Environment for use within Logback. Doing so can be useful if you want to access values from your application.properties file in your Logback configuration.

The tag works in a similar way to Logback’s standard <property> tag. However, rather than specifying a direct value, you specify the source of the property (from the Environment). If you need to store the property somewhere other than in local scope, you can use the scope attribute. If you need a fallback value (in case the property is not set in the Environment), you can use the defaultValue attribute. The following example shows how to expose properties for use within Logback:

1
2
3
4
5
6
<springProperty scope="context" name="fluentHost" source="myapp.fluentd.host" defaultValue="localhost"/>

<appender name="FLUENT" class="ch.qos.logback.more.appenders.DataFluentAppender">
<remoteHost>${fluentHost}</remoteHost>
...
</appender>

Developing Web Applications

Spring Boot is well suited for web application development. You can create a self-contained HTTP server by using embedded Tomcat, Jetty, Undertow, or Netty. Most web applications use the springboot-starter-web module to get up and running quickly. You can also choose to build reactive web applications by using the spring-boot-starter-webflux module.

Spring Web MVC Framework

The Spring Web MVC framework (often referred to as simply “Spring MVC”) is a rich “model view controller” web framework. Spring MVC lets you create special @Controller or @RestController beans to handle incoming HTTP requests (@Controller would be returning data that is being rendered by view, and @RestController would be returning raw data). Methods in your controller are mapped to HTTP by using @RequestMapping annotations.

The following code shows a typical @RestController that serves JSON data (by default):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@RequestMapping(value="/users")
public class MyRestController {
@RequestMapping(value="/{user}", method=RequestMethod.GET)
public User getUser(@PathVariable Long user) { // objects returned are turned into JSON format
// ...
}

@RequestMapping(value="/{user}/customers", method=RequestMethod.GET)
List<Customer> getUserCustomers(@PathVariable Long user) {
// ...
}

@RequestMapping(value="/{user}", method=RequestMethod.DELETE)
public User deleteUser(@PathVariable Long user) {
// ...
}
}

Spring MVC is part of the core Spring Framework, and detailed information is available in the reference documentation. There are also several guides that cover Spring MVC available at spring.io/guides.

Spring Boot Framework

Since Spring Boot has all those AutoConfiguration classes doing the configuration work for us, it is much simpler and faster for us to develop a web application using Spring Boot. In summary, all we need to do is:

  1. Create a Spring Boot project (if you use Spring Initialzr, you will see that it is based on Spring Boot) with your desired dependencies
  2. Spring Boot will load all those AutoConfiguration files, which you can further customize using application.properties or application.yml
  3. Write your own program

The most important step is to understand the mechanism behind AutoConfiguration, which uses the xxxAutoConfiguration classes that has properties injected (identified with a certain prefix).

Spring Boot Web Application Project Structure

WebJars

WebJars is simply taking the concept of a JAR and applying it to client-side libraries or resources. For example, the jQuery library may be packaged as a JAR and made available to your Spring MVC application. There are several benefits to using WebJars, including support for Java build tools such as Gradle and Maven.

For Spring Boot to load and later package your web contents correctly, you need to add your contents at a correct place. If you look into your WebMVCAutoConfiguration class, you will see the following method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
} else {
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
if (!registry.hasMappingForPattern("/webjars/**")) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[] "/webjars/**"}).addResourceLocations(new String[]{"classpath:/META-INF/resources/webjars/"}).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
}

String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
}

}
}

So you see that all your web contents need to be placed in classpath:/META-INF/resources/webjars/ directory. This means that within your project, any file stored under /META-INF/resources/webjars/ can be visited by localhost:<yourPort>/webjars/<filePath>

For example, on the https://www.webjars.org/ you can choose which webjars to import into your project by copying the following to your pom file.

1
2
3
4
5
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>4.0.0</version>
</dependency>

Then in your external dependency, you can see that it also has the structure mentioned above, and you can visit its resources with localhost:8080/webjars/bootstrap/4.0.0/webjars-requirejs.js, for example. Then you will see that js file displayed:

1
2
3
4
5
6
7
8
9
10
/*global requirejs */

// Ensure any request for this webjar brings in jQuery.
requirejs.config({
paths: {
"bootstrap": webjars.path("bootstrap", "js/bootstrap"),
"bootstrap-css": webjars.path("bootstrap", "css/bootstrap")
},
shim: { "bootstrap": [ "jquery" ] }
});
Static contents

In the same method where you find those webjar structure, you can also find where you should store your static contents:

1
2
3
4
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
}

which finds your resource under the following directories:

1
2
3
4
5
"classpath:/META-INF/resources/", 
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
"/" (project root path)

This means that if a request is not handled, you will be redirected to the static files under those locations.

For example, if you have an html page under the following directory:

staticdirectory.png

Then you can visit the html by `localhost:8080/signin/index.html

Welcome Page

Welcome page is basically the home page of your application. Spring Boot also has a default mapping defined in the WebMvcAutoConfiguration class:

1
2
3
4
5
6
7
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations());
return welcomePageHandlerMapping;
}

This in the end will look for index.html below all static content directories mentioned above, and maps to `/`.**

For example, if you have the following structure:

homepagedirectory.png

Then you can visit this index.html by localhost:8080/

Configure Your Web Properties

If you want to customize those locations, you can also use the spring.resources.xxx to modify the default paths scanned.

This works because in the ResoucrProperties class, Spring Boot defines locations for scanning static resources as shown below:

1
2
3
4
5
6
@ConfigurationProperties(
prefix = "spring.resources",
ignoreUnknownFields = false
)
public class ResourceProperties {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};

This means you can change those locations by specifying in your application.properties or applications.yml:

1
spring.resources.static-locations=classpath:/hello/, classpath:/example

Using Web Templates

Web templates can be used to generate dynamic contents based on static templates. The basic mechanism is shown below:

web-template-engine.png

Common template engines include JSP, Velocity, Freemarker,Thymeleaf, among which Thymeleaf is the default template engine used by Spring Boot.

Importing Thymeleaf

Since we are using Spring Boot with Maven, it is easy to include thymeleaf into your project. If you have already inherited the parent spring-boot-starter , then you just need to include the following dependency in your pom

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

However, this uses the version 2.3.1 (specified in the parent pom). It is recommended to use thymeleaf 3 or above for more functionalities. To do this, you need to configure the properties in your pom.

1
2
3
4
5
<properties>
<!-- This is how you change your Thymeleaf version -->
<thymeleaf.version>3.0.11.RELEASE</thymeleaf.version>
<thymeleaf-layout-dialect.version>2.1.1</thymeleaf-layout-dialect.version>
</properties>
Using Thymeleaf

The simplest example of using Thymeleaf would be to return an html page upon a request.

If we look into the ThymeleafProperties class (located in SpringBootAutoConfigure jar, under the folder thymeleaf), you will see:

1
2
3
4
5
6
7
8
9
10
11
12
13
@ConfigurationProperties(
prefix = "spring.thymeleaf"
)
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING;
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
private boolean checkTemplate = true;
private boolean checkTemplateLocation = true;
private String prefix = "classpath:/templates/";
private String suffix = ".html";
private String mode = "HTML";
...

where

1
2
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";

means that Thymeleaf scans through classpath:/templates/ for files ending with .html to be rendered.

Now, to hand you back a html page based on request using Thymeleaf, all you need to do is add a html page with the same return result as your html in your templates folder:

For example, if you have

thymeleaf-html.png

Then your handler could look like:

1
2
3
4
5
6
7
@Controller
public class HTMLRequests {
@RequestMapping("/getHtmlPage")
public String testHTMLRequest(){
return "request1"; // this has to be the same as the html file name
}
}

Note:

  • Now you should not use a @RestController, since it will automatically use the @ResponceBody which will render the page by Spring Boot itself instead of by Thymeleaf
Thymeleaf Syntax

For more information, you can follow the official thymeleaf guide.

  1. th:xxx

    • If you place th:<xxx> inside any html tag, where <xxx> could be any HTML property (including text, class, id, etc.), then Thymeleaf will replace the original value with the value you specified. For example, you can replace the text appearing in a div with:

      1
      2
      3
      4
      <!-- Value placed would be parsed as a key, whose value you need to specifiy in your Handler method -->
      <div th:text="${Hello}">
      Original Text
      </div>

      and the corresponding Java class could look like:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @Controller
      public class HTMLRequests {
      @RequestMapping("/getHtmlPage")
      public String testHTMLRequest(Map<String, String> map1){
      map1.put("Hello","Value Retrieved from key Hello"); // the value will be retrieved

      return "request1"; // this tells you which html file to return
      }
      }
  2. Precendence

    • Since there is no notion of code precedence in html, if we specified multiple th:xxx within the same tag, collision will happen. To solve this, Thymeleaf defines its own precedence rule:

      thymeleaf-precedence.png

  3. Thymeleaf Expressions

    • Simple expressions:
          Variable Expressions: ${...}
              # can be used to read/store objects, and invoking certain methods on them
          Selection Variable Expressions: *{...}
              # mainly used in combination with th:object, which stores an object in the current scope
              # then using *{...} will select/read a certain attribute of that stored object
          Message Expressions: #{...}
              # used for internationalization
          Link URL Expressions: @{...}
              # used for generating dynamic URL addresses
              # for example:
              <!-- Will produce 'http://localhost:8080/gtvg/order/details?orderId=3' (plus rewriting) -->
              <a href="details.html" th:href="@{http://localhost:8080/gtvg/order/details(orderId=${o.id})}">view</a>
      
              <!-- Will produce '/gtvg/order/details?orderId=3' (plus rewriting) -->
              <a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a>
          Fragment Expressions: ~{...}
              # this will be talked about later 
      Literals:
          Text literals: 'one text' , 'Another one!' ,…
          Number literals: 0 , 34 , 3.0 , 12.3 ,…
          Boolean literals: true , false
          Null literal: null
          Literal tokens: one , sometext , main ,…
      Text operations:
          String concatenation: +
          Literal substitutions: |The name is ${name}|
      Arithmetic operations:
          Binary operators: + , - , * , / , %
          Minus sign (unary operator): -
      Boolean operations:
          Binary operators: and , or
          Boolean negation (unary operator): ! , not
      Comparisons and equality:
          Comparators: > , < , >= , <= ( gt , lt , ge , le )
          Equality operators: == , != ( eq , ne )
      Conditional operators:
          If-then: (if) ? (then)
          If-then-else: (if) ? (then) : (else)
              # same as Java tertiary operation
          Default: (value) ?: (defaultvalue)
      Special tokens:
          No-Operation: _
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21

      (For more information, please visit Chapter 4 of the [Thymeleaf Documentation](https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html))


      Example:

      In your Java program, you can have:

      ```java
      @Controller
      public class HTMLRequests {
      @RequestMapping("/getHtmlPage")
      public String testHTMLRequest(Map<String, String> map1, Map<String, Object> map2){
      map1.put("Hello","<h3>Value Retrieved from key Hello with h3 heading<h3>");

      String[] names = {"jason","michael","bowen"};
      List users = Arrays.asList(names);
      map2.put("Users",users);
      return "request1";
      }
      }

In your html code, you can have:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"> <!-- Adding this line enables syntax correction/suggestion -->
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Test Request</h1>
<p> A paragraph here </p>
<!-- th:text configures/replaces the text appearing in the div to display value that maps to key=hello -->
<!-- th:utext does not translate html element tags, which means if you had h1 tag in your value, it will be displayed as h1 -->
<div th:utext="${Hello}">Original text</div>
<hr/>

<!-- This for each will generate multiple h5 tags -->
<h5 th:text="${user}" th:each="user:${Users}"></h5>
<hr/>

<h5>
<span th:each="user:${Users}">[[${user}]] </span>
</h5>

</body>
</html>

where

  • th:each can be seen as a for loop iteration, that does the work of for element: iterable.
  • [[${}]] is the inline version of th:text=${}. [(${})] is the inline version of th:utext="${}"
Internationalization Example

To enable your website to show contents in different languages based on users’ preferences (e.g. browser language used), you need to use internationalization, which is supported by Spring Boot. (In Spring MVC, this is configured by MessageResouce class. However, in Spring Boot, it is configured automatically with MessageSourceAutoConfiguration.)

In summary, you need to do the following steps:

  • Create a new folder under your resource folder, in which you will store all your internationalization properties file.
  • Create properties file in the format of <xxx>.properties, which will be used as the default properties file for that specific page xxx (for example).
  • Create other properties file in the format of <xxx>_<language>_<CountryCode>.properties. In this way, IntelliJ will recognize them, knowing that you are doing internationalization.
  • Now, since IntelliJ recognized them as internationalization files, you can use the Resource Bundle option when editing your properties file.
  • After configuring your properties file, you need to specify the base path (e.g. a property under the location resources/i18n/login_en_US.properties have the base path of i18n.login ) of your properties file with spring.messages.basename=<yourBasePath> in your application.properties.
  • Finally, you just need to go to your html page and use the thmeleaf messages expression #{...} (for example, if you had a property named login.title=... in your login_en_US.properties file, you should use #{login.title}) to render your message expression text automatically based on your browser language.

For example:

  • if you have done step 1-4 correctly, you should be working like this:

interationalization-example.png

Note:

  • If you used Chinese in your properties files, and you get unreadable codes, you might need to change your decoding to enable ASCII conversion. For example, you can try the following:

    change_encoding.png

If you want to achieve the language swapping by a button on your page, you need to use the LocaleResolver class (also inside the MessageResouceAutoConfiguration class).

By default, this works by getting the section Accept-Language in the request header when you send your html request. To override this behavior, you need to write your own LocaleResolver class.

For example,

  1. first in your html page, you would have a button that creates those request with language:
1
2
3
<!-- this will be parsed as index.html?l=zh_CN, where index.html would be this same page -->
<... th:href="@{/index.html(l="zh_CN")}">中文</...>
<... th:href="@{/index.html(l="en_US")}">English</...>
  1. In your MyLocaleResolver class, you can write:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyLocaleResolver implements LocaleResolver{
@Override
public Locale resolveLocale(HttpServletRequest request){
String lang = request.getParameter("l"); // getting the information l=zh_Ch from the request, for example
Locale locale = Locale.getDefault(); // if there is none, then gets the default Locale information
if(!StringUtils.isEmpty(l)){
String[] split = l.split("_"); // gets the language and country code
locale = new Locale(split[0]. split[1]);
}
return locale;
}

@Override
public void setLocale(...){
...
}
}
  1. Add this component to your application as a bean. This is because the Spring Boot LocaleResolver is injected with the condition: @ConditionalOnMissingBean, meaning that if we injected this bean, then Spring Boot’s default resolver will not be activated:
1
2
3
4
5
// in one of your @Configuration class, for example
@Bean
public LocaleResolver localeResolver(){
return new MyLocaleResolver();
}

Sending and Handling Post Request

To handle a user login, for example, you need to do two parts (in abstract).

  1. First, in your HTML page, you need to send a request (th:action="@{<xxx>}") with your form, in which you have each field that you need to be set with a specific name=xxx attribute.
  2. Then, you add a handler to with PostController() to map to that request, and gets the value in those fields with @RequestParam("<xxx>") String xxx.

For example:

In your html page, you can have:

1
2
3
4
5
6
<form class="..." action="..." th:action="@{/user-login}" method="post">
...
<input type="..." class="..." placeholder="..." name="username">
<input type="..." class="..." placeholder="..." name="password">
...
</form>

In your Java class, you can have:

1
2
3
4
5
6
7
8
9
10
@Controller
public class LoginController{
// same as @RequestMapping(method=RequestMethod.POST)
@PostMapping(value="/user-login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password){
... // your logic to check those username and passwords
return "dashboard" // the page in templates folder that you want to show to logged in users
}
}

Note:

  • If hot swapping is not working (assuming you have enabled it) when you changed those static/templates html code, you should try:
      1. add the line spring.thymeleaf.cache=false in your application.properties file
      2. use Ctrl+F9 or manually click the build button to rebuild your project

If you want to add the functionality of popping up red text in your html page with Login Failed when your user failed the authentication, you can use the th:if functionality with some small changes to your controller code:

In your controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
public class LoginController{
// same as @RequestMapping(method=RequestMethod.POST)
@PostMapping(value="/user-login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password
Map<String, String> errmsg){
... // your logic to check those username and passwords
if(failed to authenticate){
errmsg.put("msg","Login Failed");
return "login" // your original page
}
return "dashboard" // the page in templates folder that you want to show to logged in users
}
}

Then in your html code, you can have:

1
2
<!-- For example, you can place in inside the form you use to submit login information -->
<p style="color: red" th:text="${msg}" th:if="not #strings.isEmpty(msg)"></p>

@RequestParam/@PathParam/@Param

The difference between @RequestParam and @PathParam is best illustrated from using an example.

The general difference between the two can be seen from the following html request:

1
localhost:8080/home/<PathParam>/somethingelse?<RequestParam>=something

Therefore, suppose you made a html request:

1
localhost:8080/home/jason/somethingelse?order=something

Then you can get the parameter jason and order=something with:

1
2
3
4
5
6
@RequestMapping(value = "/home/{user}/somethingelse", method = RequestMethod.GET)
@ResponseBody
public String getOrder(@PathVariable final String user,
@RequestParam String order) {
return ...;
}

This makes it clear, so that :

  • @PathVariable gets the variable in the path with the format {<VariableName>}
  • @RequestParam gets the variable in the request parameter (the key-value pair after the query ? punctuation)

On the other hand, @Param does the reverse job of making a specific variable in the method available for outer access.

For example:

1
2
3
4
@Mapper
public interface UserMapper {
List<User> listUser(@Param(value = "userName") String userName);
}

Then when this method is called with an input argument, you can get it in a xml mapper, for example:

1
2
3
4
5
6
7
8
9
10
<!-- Other section of this xml ignored for simplicity -->
<select id="listUser" parameterType="java.lang.String" resultMap="userResultMap">
select * from User
<where>
<!-- This is where userName variable is used -->
<if test="userName != '' and userName != null">
name like CONCAT(#{userName},'%')
</if>
</where>
</select>

Spring Boot MVC Extensions

Spring Boot provides auto-configuration for Spring MVC that works well with most applications. The auto-configuration adds the following features on top of Spring’s defaults:

  • Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
  • Support for serving static resources, including support for WebJars (covered before)).
  • Automatic registration of Converter, GenericConverter, and Formatter beans.
    • For example, converting an object to json format
  • Support for HttpMessageConverters.
    • For example, converts and handles http request
  • Automatic regis tration of MessageCodesResolver (covered later in this document).
  • Static index.html support (covered before)
  • Custom Favicon support
  • Automatic use of a ConfigurableWebBindingInitializer bean.

If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.

If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.

If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.

Example: Adding a ViewController

If you want to control a certain request to show the output of another request, you can use a ViewController to control the view of a page. However, by default Spring Boot does not have AutoConfiguration classes for that. Therefore, you would need to add your own ViewController by “adding your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc“ (mentioned above).

For instance, to map the request /request to request1 (which you specified as a html page configured by Thymeleaf), you need to write the configuration class:

1
2
3
4
5
6
7
@Configuration
public class myMVCExtensionConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/request").setViewName("request1");
}
}

Mechanism

Since WebMvcAutoConfiguration is the AutoConfiguration class for Spring MVC, we can look into that class to understand those extension mechanisms. Within that class, you will see:

1
2
3
4
5
6
@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
@EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class})
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
...
}

Which imports WebMvcAutoConfiguration.EnableWebMvcConfiguration.class, and that class extends DelegatingWebMvcConfiguration by having the following signature:

1
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {

Finally, in the parent class, you will see:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Autowired(
required = false
) // since it is AutoWired, those configures are obtained from the IoC container
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}

}

protected void addViewControllers(ViewControllerRegistry registry) {
this.configurers.addViewControllers(registry);
}

And finally if we trace the configureViewResolvers method, we will see:

1
2
3
4
5
6
7
8
9
10
public void addViewControllers(ViewControllerRegistry registry) {
Iterator var2 = this.delegates.iterator();

while(var2.hasNext()) {
WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next();
delegate.addViewControllers(registry); // calls the method addViewControllers on each WebMvcConfigurer class
// and add them to the registry object
}

}

This is why we needed to extend WebMvcConfigurer class for extending its functionality, add calling the method addViewControllers() in the above example to add our ViewController.

Graceful shutdown

Graceful shutdown is supported with all four embedded web servers (Jetty, Reactor Netty, Tomcat, and Undertow) and with both reactive and Servlet-based web applications. It occurs as part of closing the application context and is performed in the earliest phase of stopping SmartLifecycle beans.

This stop processing uses a timeout which provides a grace period during which existing requests will be allowed to complete but no new requests will be permitted. The exact way in which new requests are not permitted varies depending on the web server that is being used. Jetty, Reactor Netty, and Tomcat will stop accepting requests at the network layer. Undertow will accept requests but respond immediately with a service unavailable (503) response.

To enable graceful shutdown, configure the server.shutdown property, as shown in the following example:

1
server.shutdown=graceful

Working With Databases

Introduction

Spring Boot allows for both the use of relational databases and non-relational databases. This is achieved by using the Spring Data project, which handles all those requests for us by enabling a number of auto configurations, including some database settings. For more information, you can look at the official documentation.

Using JDBC

Connecting to databases in Spring Boot is generally easy to do. All you need to make sure is that:

  1. Your IP address is valid for connection to your remote database (applies only if you are using a remote database)
  2. You configured your login credentials properly in application.yml or application.properties
  3. Finally you can just use aautowired DataSource object in Spring Boot to establish a connection.

Adding JDBC in Maven Dependency

To use JDBC, you need to add the following dependencies:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

Configuring Your Login Credentials

You can configure your login credentials either in application.yml or in application.properties

1
2
3
4
5
6
spring:
datasource:
username: <login-username>
password: <login-password>
url: jdbc:mysql://<your-database-ip>:<your-database-port>/<database-name>
driver-class-name: com.mysql.cj.jdbc.Driver

Creating a Connection

Since we are testing the connection for now (not using it yet), we can do this in our test class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootTest
class Testsb2ApplicationTests {
@Autowired
DataSource dataSource; // the database connection object

@Test
void contextLoads() {
System.out.println(dataSource.getClass());
try {
Connection connection = dataSource.getConnection();
System.out.println(connection);
connection.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}

And if things are running smoothly, you should get something like this:

1
2
3
2020-07-06 14:09:36.306  INFO 24324 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2020-07-06 14:09:37.972 INFO 24324 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
HikariProxyConnection@26391950 wrapping com.mysql.cj.jdbc.ConnectionImpl@18d86e4

Note:

  • If you get connection errors, but you are certain that your login username/password are correct, then it could be that your machine’s IP address has not been approved in the SQL server (again, this should happen only if you are connecting to a remote database). To approve connections from your machine, you need to run the following commands in your SQL:

    1
    CREATE USER '<your-username>'@'<your-machines-IP-address>' IDENTIFIED BY '<your-password>';

    then:

    1
    GRANT ALL PRIVILEGES ON *.* TO '<your-username>'@'<your-machines-IP-address>' WITH GRANT OPTION;

    finally, to make sure those changes are updated:

    1
    flush privileges;

Spring Boot JDBC DataSource Mechanism

How does all the above work? How to I now use that connection to run SQL queries and scripts?

If you go to the org.springframework.boot.autoconfigure.jdbc package, you will see the class DataSourceConfiguration. Parts of that class is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnMissingBean({DataSource.class})
@ConditionalOnProperty(
name = {"spring.datasource.type"}
)
static class Generic {
Generic() {
}

@Bean
DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build(); // notice the initializeDataSourceBuilder() method
}
}

Now, if we trace that initializeDataSourceBuilder(), you will see:

1
2
3
4
5
// A DataSourceBuilder object is returned, which contains all those login information
// notice the create() method that is called
public DataSourceBuilder<?> initializeDataSourceBuilder() {
return DataSourceBuilder.create(this.getClassLoader()).type(this.getType()).driverClassName(this.determineDriverClassName()).url(this.determineUrl()).username(this.determineUsername()).password(this.determinePassword());
}

Another important class is DataSourceAutoConfiguration, which (as the name suggests) contains default configurations that Spring Boot uses, as well as telling your which configurations can be overridden in your application.properties or application.yml file. Part of the class is shown below:

1
2
3
4
5
6
7
public class DataSourceAutoConfiguration {
public DataSourceAutoConfiguration() {
}

static class EmbeddedDatabaseCondition extends SpringBootCondition {
private static final String DATASOURCE_URL_PROPERTY = "spring.datasource.url";
private final SpringBootCondition pooledCondition = new DataSourceAutoConfiguration.PooledDataSourceCondition();

Lastly, for running scripts, if you go to DataSourceInitialzer class, you will see:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class DataSourceInitializer {
...
boolean createSchema() {
List<Resource> scripts = this.getScripts("spring.datasource.schema", this.properties.getSchema(), "schema");
if (!scripts.isEmpty()) {
if (!this.isEnabled()) {
logger.debug("Initialization disabled (not running DDL scripts)");
return false;
}

String username = this.properties.getSchemaUsername();
String password = this.properties.getSchemaPassword();
this.runScripts(scripts, username, password);
}

return !scripts.isEmpty();
}

void initSchema() {
List<Resource> scripts = this.getScripts("spring.datasource.data", this.properties.getData(), "data");
if (!scripts.isEmpty()) {
if (!this.isEnabled()) {
logger.debug("Initialization disabled (not running data scripts)");
return;
}

String username = this.properties.getDataUsername();
String password = this.properties.getDataPassword();
this.runScripts(scripts, username, password);
}

}

This means that you can run scripts or data by specifying the property spring.datasource.schema or spring.datasource.data, or, if you names your scripts with schema-<xxx>.sql and data-xxx.sql, Spring Boot will automatically run those for you.

Example: Running a Schema Script

Now, suppose if you have a SQL script called users.sql, which creates a table called users, you can run it by:

  1. Placing that file under your resource folder in your project. In this example, it is placed under resources/sql/users.sql
  2. Add the line spring.datasource.schema in your properties or yml file:
1
2
3
4
5
6
7
8
spring:
datasource:
username: testuser
password: 12345
url: jdbc:mysql://192.168.217.100:3306/testjdbc
driver-class-name: com.mysql.cj.jdbc.Driver
schema:
- classpath:sql/users.sql

Note:

  • Since spring.datasource.schema takes in arguments as a list, you need to input your files with - <your-script-path> format, as shown above. This also means that you can run multiple scripts easily.

Using JDBC for SQL Query

First, before jumping right into using it, we should look at some source code in JdbcTemplateAutoConfiguration.

1
2
3
4
5
6
7
8
9
@ConditionalOnClass({DataSource.class, JdbcTemplate.class})	// notice here
@ConditionalOnSingleCandidate(DataSource.class)
@AutoConfigureAfter({DataSourceAutoConfiguration.class})
@EnableConfigurationProperties({JdbcProperties.class})
@Import({JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class}) // and here
public class JdbcTemplateAutoConfiguration {
public JdbcTemplateAutoConfiguration() {
}
}

At this point, we certainly have and are using a DataSource class. This means that Spring Boot will automatically import the class JdbcTemplateConfiguration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class JdbcTemplateConfiguration {
...
@Bean
@Primary
JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
Template template = properties.getTemplate();
jdbcTemplate.setFetchSize(template.getFetchSize());
jdbcTemplate.setMaxRows(template.getMaxRows());
if (template.getQueryTimeout() != null) {
jdbcTemplate.setQueryTimeout((int)template.getQueryTimeout().getSeconds());
}

return jdbcTemplate;
}
}

And finally, we see that we get a bean JdbcTemplate, which will be the main tool that we will use for querying a database.

Now, to use that JdbcTemplate to query for something, we can create a controller:

1
2
3
4
5
6
7
8
9
10
11
12
@Controller
public class HelloController{
@AutoWired
JdbcTemplate jdbcTemplate;

@ResponceBody
@GetMapping("/testQuery")
public Map<String, Object> getFirstMap(){
List<Map<String, Object>> data = jdbcTemplate.queryForList("select * from users");
return data.get(0); // returns the first instance
}
}

Then if you go your browser and try it, you will get your data (assuming if you have any) packaged into a map format returned to you as a json. For example, if you have:

1
2
3
4
5
+------+-------+------+
| id | name | sex |
+------+-------+------+
| 1 | jason | m |
+------+-------+------+

Then this entry will be returned as a map, in which the column name are the keys, and the values are the values in the map.

1
{"id":1,"name":"jason","sex":"m"}

This also explains why you had the line List<Map<String, Object>> data, since each entry row will be retrieved as a map object, and a collection of those map objects is then stored into a List.

Using Druid DataSource

DataSource is a name given to the connection set up to a database from a server. The name is commonly used when creating a query to the database.

There are potentially many DataSources available, for example Hikari, which has a very good performance. However, this guide will focus on Druid, which is a DataSource made by Alibaba, known for its security and monitoring features (in the expense of some performance).

Adding Druid to your Maven Dependency

Include the following in your pom.xml.

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>

The latest version can be found at https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter/1.1.23

Note:

And then add the property spring.datasource.type=com.alibaba.druid.pool.DruidDataSource. For example, in the application.yml, it could look like:

1
2
3
4
5
6
7
spring:
datasource:
username: testuser
password: 12345
url: jdbc:mysql://192.168.217.100:3306/testjdbc
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource

Configuring Your Druid DataSource

At this point, if you run your test class that you wrote before:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SpringBootTest
class Testsb2ApplicationTests {
@Autowired
DataSource dataSource;

@Test
void contextLoads() {
System.out.println(dataSource.getClass());
try {
Connection connection = dataSource.getConnection();
System.out.println(connection);
connection.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}

}

You will see the line:

1
2020-07-06 16:11:07.865  INFO 3200 --- [           main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited

which means that now the DataSource has been switched to Druid. However, if you remember the DataSourceProperties class, which binds all the properties from your properties or yml file, you will see:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@ConfigurationProperties(
prefix = "spring.datasource"
)
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
// this means that only those will be binded
private ClassLoader classLoader;
private String name;
private boolean generateUniqueName = true;
private Class<? extends DataSource> type;
private String driverClassName;
private String url;
private String username;
private String password;
private String jndiName;
private DataSourceInitializationMode initializationMode;
private String platform;
private List<String> schema;
private String schemaUsername;
private String schemaPassword;
private List<String> data;
private String dataUsername;
private String dataPassword;
private boolean continueOnError;
private String separator;
private Charset sqlScriptEncoding;
private EmbeddedDatabaseConnection embeddedDatabaseConnection;
private DataSourceProperties.Xa xa;
private String uniqueName;

public DataSourceProperties() {
this.initializationMode = DataSourceInitializationMode.EMBEDDED;
this.platform = "all";
this.continueOnError = false;
this.separator = ";";
this.embeddedDatabaseConnection = EmbeddedDatabaseConnection.NONE;
this.xa = new DataSourceProperties.Xa();
}

Now, we see the problem that some properties of the Druid package is not specified in the class above. This means that those values will not be injected/bound to the DataSource object created. For example, if you have specified:

1
2
3
4
5
6
7
8
9
10
11
12
spring:
datasource:
username: testuser
password: 12345
url: jdbc:mysql://192.168.217.100:3306/testjdbc
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000

Then if you run the debug mode of your test class and look at that DataSource file created, you will see:

1
2
3
4
initialSize = 0
maxActive = 8
minIdle = 0
maxIdle = 8

which is obviously not what you wanted.

Therefore, this means that we need to write our own DataSource bean to inject into the container, replacing the default DataSource class above, and binding all those additional properties:

1
2
3
4
5
6
7
8
@Configuration
public class MyDruidConfig {
@ConfigurationProperties(prefix="spring.datasource.druid") // binding those druid additional properties
@Bean
public DataSource druid(){
return new DruidDataSource();
}
}

But you also need to change other fields such as username and password (and all those connection parameters) to:

1
2
3
4
5
6
7
8
9
10
11
12
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
username: testuser
password: 12345
url: jdbc:mysql://192.168.217.100:3306/testjdbc
driver-class-name: com.mysql.cj.jdbc.Driver
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000

As otherwise those cannot be bounded correctly since you specified @ConfigurationProperties(prefix="spring.datasource.druid").

Now, if you re-run the test class, you will see the DataSource object to have:

1
2
3
4
maxActive = 0
minIdle = 5
maxIdle = 8
maxWait = 60000

And everything else would work just fine.

Configuring Druid Monitor

To enable and use the monitor in druid, you need to:

  1. Create a Servlet that manages the requests to the backend
  2. Create a Filter that filters those requests

First, to create a StatViewServlet to monitor your requests, you need to register it as a Servlet and inject it into your application. But what can be configured from that StatViewServlet? We can go to the StatViewServlet class:

1
2
3
4
5
6
7
8
9
10
11
12
public class StatViewServlet extends ResourceServlet {
private static final Log LOG = LogFactory.getLog(StatViewServlet.class);
private static final long serialVersionUID = 1L;
public static final String PARAM_NAME_RESET_ENABLE = "resetEnable";
public static final String PARAM_NAME_JMX_URL = "jmxUrl";
public static final String PARAM_NAME_JMX_USERNAME = "jmxUsername";
public static final String PARAM_NAME_JMX_PASSWORD = "jmxPassword";
private DruidStatService statService = DruidStatService.getInstance();
private String jmxUrl = null;
private String jmxUsername = null;
private String jmxPassword = null;
private MBeanServerConnection conn = null;

More can be seen in that ResoucrServlet class as well:

1
2
3
4
5
6
7
8
public abstract class ResourceServlet extends HttpServlet {
private static final Log LOG = LogFactory.getLog(ResourceServlet.class);
public static final String SESSION_USER_KEY = "druid-user";
public static final String PARAM_NAME_USERNAME = "loginUsername";
public static final String PARAM_NAME_PASSWORD = "loginPassword";
public static final String PARAM_NAME_ALLOW = "allow";
public static final String PARAM_NAME_DENY = "deny";
public static final String PARAM_REMOTE_ADDR = "remoteAddress";

Therefore, some of your configuration can be:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// in a @Configuration class
@Bean
public ServletRegistrationBean servletRegistrationBean(){
// register it as a servlet
ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/test-druid/");

Map<String, String> initParam = new HashMap<>();
initParam.put("loginUsername","testServletUser"); // notice the key has to match the parameters
initParam.put("loginPassword","2133");
initParam.put("allow","192.168.217.1");

bean.setInitParameters(initParam);
return bean;
}

Second, to create a filter for filtering which requests to be intercepted, you can use the WebStatFilter.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Preferably the same configuration class as above
@Bean
public FilterRegistrationBean webStatFilter(){
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new WebStatFilter());

Map<String, String> initParams = new HashMap<>();
initParams.put("exclusions","/test-druid/*"); // all those requests are excluded for interception

bean.setInitParameters(initParams);
bean.setUrlPatterns(Arrays.asList("/*"));
return bean;
}

Some of the parameters used above can be found in the WebStatFilter class:

1
2
3
4
5
6
7
8
9
10
public class WebStatFilter extends AbstractWebStatImpl implements Filter {
private static final Log LOG = LogFactory.getLog(WebStatFilter.class);
public static final String PARAM_NAME_PROFILE_ENABLE = "profileEnable";
public static final String PARAM_NAME_SESSION_STAT_ENABLE = "sessionStatEnable";
public static final String PARAM_NAME_SESSION_STAT_MAX_COUNT = "sessionStatMaxCount";
public static final String PARAM_NAME_EXCLUSIONS = "exclusions";
public static final String PARAM_NAME_PRINCIPAL_SESSION_NAME = "principalSessionName";
public static final String PARAM_NAME_PRINCIPAL_COOKIE_NAME = "principalCookieName";
public static final String PARAM_NAME_REAL_IP_HEADER = "realIpHeader";
protected PatternMatcher pathMatcher = new ServletPathMatcher();

Now, if you visit localhost:8080/test-druid/, you will be redirected to localhost:8080/test-druid/login.html, and you can enter your login credentials there. If you enter the login credentials specified in the StatViewServlet class, you should be redirected to the monitor page.

Using Mybatis

(additional reference: https://www.baeldung.com/mybatis; https://mybatis.org/mybatis-3/getting-started.html)

Adding Mybatis in Maven Dependency

To make use of MyBatis we need to add the dependency to our pom.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>

<!-- The driver to connect to databases -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

The latest version of the mybatis-spring dependency can be found here.

Configuring your DataSource Connection

This is essentially the same as the previous section. If you use the default DataSource, you only need to specify a few parameters in your application.yml. However, if you want to use the Druid DataSource, you need to also include a @Configuration class to configure that Druid DataSource properly as shown above.

Creating Components/Beans corresponding to your Table

Now, you need to create Beans/Objects that correspond to your table.

Suppose you have a table in one of your databases looking like this:

id departmentName
1 Science

Then, your corresponding Java class should look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.testmybatis.components;

public class Department {

private Integer id; // this must be the same as the column name
private String departmentName; // this must be the same as the column name

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getDepartmentName() {
return departmentName;
}

public void setDepartmentName(String departmentName) {
this.departmentName = departmentName;
}
}

Creating Mapper Annotations to Enable MyBatis Queries

Now, to make use of that Department object and query for data, we need to create a @Mapper class that tells Spring Boot MyBatis that this will be mapping/making queries/query data.

For example, some common operations would look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.testmybatis.mappers;

import com.example.testmybatis.components.Department;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper // tells Spring Boot that this is a MyBatis Mapper
public interface DepartmentMapper { // notice that this is an Interface

@Select("select * from Department where id=#{inputId}")
public Department getDeparmentById(Integer inputId);

@Delete("delete from Department where id=#{inputId}")
public int deleteDepartmentById(Integer inputId);

@Insert("insert into Department values (#{id},#{departmentName})")
public int insertDeparment(Department department);
}

And then you just need to have a controller that first has an autowired DepartmentMapper, and then uses that to create queries:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.example.testmybatis.controller;

import com.example.testmybatis.components.Department;
import com.example.testmybatis.mappers.DepartmentMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DepartmentController {

@Autowired
DepartmentMapper departmentMapper;

@GetMapping("/dept/{id}")
public Department getDepartment(@PathVariable("id") Integer id){
return departmentMapper.getDeparmentById(id);
}

@GetMapping("/dept")
public Department insertDepartment(Department department){ // this argument will be provided as part of the request
departmentMapper.insertDeparment(department);
return department;
}
}

Note:

  • At this point, you should see that IntelliJ tells you that there exists an error that DepartmentMapper departmentMapper cannot be auto-wired. This is because we created the mapper interface instead of a class. However, this is exactly how MyBatis works, so even if IntelliJ shows you this potential error, you should still be able to build this and get this to work.

To get the first entry, you can use http://localhost:8080/dept/1

To insert a second entry, you can use http://localhost:8080/dept?id=2&&departmentName=Math

Note:

  • If you have used an id field in your database that is increment automatically, and in your mapper class you only specified the need to insert a departmentName, then by default that Department object will not know about the auto-incremented id. Therefore, to display correct result, you need to add the annotation @Options(useGeneratedKeys = true, ketProperty = <thatField>) above that @Insert query. For example:

    1
    2
    3
    @Options(useGeneratedKeys = true, keyProperty = "id")
    @Insert("insert into Department values (#{departmentName})")
    public int insertDeparment(Department department);

Creating Mapper XML to Enable MyBatis Queries

If you use xml to create mappers, it allows for more functionalities including Dynamic SQL. However, it does require you to create an additional xml file.

If you use xml to create mappers, your mapper java class will be changed to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.testmybatis.mappers;

import com.example.testmybatis.components.Department;
import org.apache.ibatis.annotations.Mapper;

@Mapper // tells Spring Boot that this is a MyBatis Mapper so that it will be scanned in automatically
public interface DepartmentMapper { // notice now that you don't use those @Select annotations

public Department getDepartmentById(String inputId); // If you use Integer here, you will get errors

public int deleteDepartmentById(String inputId); // same here

public int insertDepartment(Department department);
}

Then, the details of those queries would be in an xml file. In general, you will create your xml file in the following steps:

  1. Include the proper headings
  2. Specify your mapper namespace (which @Mapper to bind to)
  3. Specify the mappings of your return result
  4. Write queries with id bound to that method call in your @Mapper class
    1. If it has a return result, each entry you got will be a Map, which can be further mapped to the mapping you created in Step 3

For example, to get the equivalent query for @Select("select * from Department where id=#{inputId}"), you would have:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper>
<resultMap id="departmentResultMap" type=" com.example.testmybatis.components.Department">
<id column="id" jdbcType="INT" property="id"/>
<result column="department" jdbcType="VARCHAR" property="department"/>
</resultMap>

<!-- If you used parameterType="java.lang.Integer", you will get errors -->
<select id="getDepartmentById" parameterType="java.lang.String" resultMap="departmentResultMap">
select * from Department where id=#{inputId}
</select>
</mapper>

A few things to observe here are:

  • Namespace in Mapper XML should be same as Fully Qualified Name (FQN) for Mapper Interface
  • Statement id values should be same as Mapper Interface method names.
  • If the query result column names are different from bean property names we can use <resultMap> configuration to provide mapping between column names and their corresponding bean property names.

Last but not least, if you try to build and run this, you will notice that xml files tend to be not included in the Spring Boot projects. This means that you need to use plugins in pom.xml to modify this behavior:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<build>

<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

<!-- This plugin should be added -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>copy-xmls</id>
<phase>process-sources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/classes</outputDirectory>
<resources>
<resource>
<directory>${basedir}/src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>

</build>

Now, if you run mvn package, and you look into your target folder, you will find that xml file, and everything would work fine if you run the application.

Building SqlSessionFactory from XML

Every MyBatis application centers around an instance of SqlSessionFactory. A SqlSessionFactory instance can be acquired by using the SqlSessionFactoryBuilder. SqlSessionFactoryBuilder can build a SqlSessionFactory instance from an XML configuration file, or from a custom prepared instance of the Configuration class.

Building a SqlSessionFactory instance from an XML file is very simple. It is recommended that you use a classpath resource for this configuration, but you could use any InputStream instance, including one created from a literal file path or a file:// URL. MyBatis includes a utility class, called Resources, that contains a number of methods that make it simpler to load resources from the classpath and other locations.

For example, this would be how you create a SqlSessionFactory instance:

1
2
3
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

The configuration XML file contains settings for the core of the MyBatis system, including a DataSource for acquiring database Connection instances, as well as a TransactionManager for determining how transactions should be scoped and controlled. The full details of the XML configuration file can be found in later sections, but here is a simple example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>

While there is a lot more to the XML configuration file, the above example points out the most critical parts. Notice the XML header, required to validate the XML document. The body of the environment element contains the environment configuration for transaction management and connection pooling. The mappers element contains a list of mappers – the XML files and/or annotated Java interface classes that contain the SQL code and mapping definitions.

Working With JPA

Spring has support for using JPA (Java Persistence API) as well as other data access technology via its Spring Data project, which aims to simplify data access operations by providing us with easy to use APIs/Templates (for example, JDBCTemplate, RedisTemplate).

Inkedspring-data-flow.jpg

This means that all we need to do is to program towards APIs provided by Spring Data!

Importing JPA

To use JPA with Spring Boot, you need to include the following dependency in your pom,xml:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

Under the hood, Spring Boot uses Hibernate for JPA implementation.

Using JPA

Since JPA is based on the idea of ORM (Object Relational Mapping), the first thing you need to do is to create a class that maps to your data table. Then, once you have setup everything, all you need to do is to call methods from a Repository which has implemented many query implementations for you.

In summary, you need to do the following steps:

  1. Create a class that will map to the entries in your table, annotate it with @Entity, which tells JPA that it is an object that it can map to

  2. Create a interface that extends JpaRepository<T, ID> or its parent classes (e.g. CrudRepository<T, ID>, but of course the lower in hierarchy the more functionalities), annotate it with @Repository so that Spring Boot will take it in

  3. (Optional) By default, JPA has the naming strategy of converting to snake case. If you want to keep the case as it is in your Java class, you need to use:

    1
    jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
  4. Use that interface created above with @Autowire, and call existing methods from it for querying (e.g. findAllById())

For example, suppose you have a table in your database with name Product, and the following fields/columns:

id productName productPrice status
1 AA 100.00 sold

Then, your @Entity class would look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.example.testmybatis.components;

import javax.persistence.*;

@Entity // tells JPA that this is a class that maps to your database table
public class Product {

@Id // Specifies the primary key (necessary)
@GeneratedValue(strategy = GenerationType.AUTO) // because we set id to auto-increment
private Integer id;

private String productName; // notice the field names here
private Double productPrice;
private String status;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getProductName() {
return productName;
}

public void setProductName(String productName) {
this.productName = productName;
}

public Double getProductPrice() {
return productPrice;
}

public void setProductPrice(Double productPrice) {
this.productPrice = productPrice;
}

public String getStatus() {
return status;
}

public void setStatus(String status) {
this.status = status;
}
}

Your @Repository would look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.testmybatis.repository;

import com.example.testmybatis.components.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

// JpaRepository is an interface that we can use to manipulate, which extends useful classes such as CRUDRepository
// JpaRepository takes two types: the ORM class type (Product), and the Primary Key type (Integer)
@Repository
public interface ProductRepository extends JpaRepository<Product, Integer> {
}

Your application.yml for configuring your JPA looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
username: testuser
password: 12345
url: jdbc:mysql://192.168.217.100:3306/testMybatis
driver-class-name: com.mysql.cj.jdbc.Driver
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
profiles:
active: dev
# starting from here
jpa:
hibernate:
# this means create/update, so that if a DB table specified by JPA upon start up does not exist, it creates; otherwise, it updates the table
ddl-auto: update
# this keeps the name in the query the same as name in your entity class
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
# this means that it will show the SQL commands in console whenever it does something (mainly used for debugging)
show-sql: true
database: mysql

Now, everything has been setup correctly and you can use this by calling methods from your ProductRepository:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.example.testmybatis.controller;

import com.example.testmybatis.components.Product;
import com.example.testmybatis.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class ProductController {
@Autowired
ProductRepository productRepository;

@RequestMapping("/product/{id}")
public List<Product> getProduct(@PathVariable("id") Integer id){
List<Product> product = productRepository.findAllById(id);
return product;
}

@RequestMapping("/product")
public Product insertProduct(Product product){
Product save = productRepository.save(product); // adds a new product
return save;
}
}

Handling Exceptions in Spring Boot

By default, if you made a request that is not handled at all by your Spring Boot Application, a default error page will show:

default-error.png

(If you have visited this from applications other than a browser, e.g. if you have visited then the above will be shown as a json format)

Mechanism

Alike other features, this is also configured by an AutoConfiguration class, located at org.springframework.boot.autoconfigure.web.servlet.error, called ErrorMvcAutoConfiguration.

There, some important methods include:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class ErrorMvcAutoConfiguration {	
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}

@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}

@Bean
public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}

@Configuration(proxyBeanMethods = false)
static class DefaultErrorViewResolverConfiguration {
...

@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
}

Detailed Steps

  1. Whenever errors with code 4xx, 5xx take place, this error will be registered as an error page through
1
2
3
4
5
6
7
8
9
10
11
/**
* {@link WebServerFactoryCustomizer} that configures the server's error pages.
*/
static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
...
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath())); // notice the line
errorPageRegistry.addErrorPages(errorPage);
}

which comes from:

1
2
3
public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}

Notice the line:

1
2
ErrorPage errorPage = new ErrorPage(
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));

Now, if you look at the method getPath(), you will be navigated to the method:

1
2
3
public String getPath() {
return this.path;
}

which comes from:

1
2
3
4
5
6
7
8
public class ErrorProperties {

/**
* Path of the error controller.
*/
@Value("${error.path:/error}")
private String path = "/error";
...

This means that by default, when an error happens, Spring Boot will come to /error request. Now, what happens if we did not specify a handler to handler that /error request?

  1. At this point, Spring Boot will come to use the bean BasicErrorController, which is shown above in the Mechanism section as well:
1
2
3
4
5
6
7
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}

The class BasicErrorController by its name suggests that it handles all those /error request:

1
2
3
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

within this class, you will also find:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)	// notice this line
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
// This renders the error html page
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

@RequestMapping // and this
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}

In the first case, an error page produces a html page, with produces = MediaType.TEXT_HTML_VALUE. In the second case, it is not specified to produce a html page. But how does a Spring Boot know whether a request comes from a browser or other client applications?

  • This information is actually hidden in the Request Header whenever you send any requests.

    • If you send a request from a browser, you will see:

      1
      2
      3
      Request Headers:
      ...
      Accept: text/html...
    • If you send a request from other client applications, it will look like:

      1
      2
      3
      Request Headers:
      ...
      Accept: "*/*"

Lastly, the ModelAndView object is created by this method:

1
2
3
4
5
6
7
8
9
10
11
12
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
// iterates over all errorViewResolvers. If found one that maps to that error request, returns
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
// if not found, return null
return null;
}

But what is that ErrorViewResolver object?

  1. Remember the DefaultErrorViewResolver object that we have seen in the Mechanism section:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Configuration(proxyBeanMethods = false)
    static class DefaultErrorViewResolverConfiguration {

    ...

    @Bean
    @ConditionalOnBean(DispatcherServlet.class)
    @ConditionalOnMissingBean(ErrorViewResolver.class)
    DefaultErrorViewResolver conventionErrorViewResolver() { // notice here
    return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
    }

    And the DefaultErrorViewResolver class looks like:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {

    private static final Map<Series, String> SERIES_VIEWS;

    static {
    Map<Series, String> views = new EnumMap<>(Series.class);
    views.put(Series.CLIENT_ERROR, "4xx"); // This will be mentioned in the next section
    views.put(Series.SERVER_ERROR, "5xx");
    SERIES_VIEWS = Collections.unmodifiableMap(views);
    }

    ...

    private ModelAndView resolve(String viewName, Map<String, Object> model) {
    // e.g. error/404
    String errorViewName = "error/" + viewName;
    // first try to find if such a page exists in your template/or if you have a template engine
    TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
    this.applicationContext);
    if (provider != null) {
    // if it exists, return a ModelAndView object
    return new ModelAndView(errorViewName, model);
    }
    // if not, go to the method below
    return resolveResource(errorViewName, model);
    }

    private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
    // looks for static locations (your static folders)
    for (String location : this.resourceProperties.getStaticLocations()) {
    try {
    Resource resource = this.applicationContext.getResource(location);
    resource = resource.createRelative(viewName + ".html");
    if (resource.exists()) {
    return new ModelAndView(new HtmlResourceView(resource), model);
    }
    }
    catch (Exception ex) {
    }
    }
    return null;
    }

    This means that we can customize those error pages by placing the html file with that specified error code under the correct directory.

    • However, this could be troublesome if we need to create a new html for every error code. This is where the 4xx and 5xx comes in handy:
      • If you names a html page with 4xx.html, then all error codes beginning with a 4 and does not match any other <error-code>.html page you placed in that directory, then the 4xx.html will be used.
      • If you names a html page with 5xx.html, then all error codes beginning with a 5 and does not match any other <error-code>.html page you placed in that directory, then the 5xx.html will be used.
  2. Lastly, if you are using those 4xx.html or 5xx.html, how do you correctly find out the actual error code to display on your page? In other words, where are those attributes stored?

    This is explained by the class we mentioned above in the section Mechanism, called DefaultErrorAttributes:

    1
    2
    3
    4
    5
    @Bean
    @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
    public DefaultErrorAttributes errorAttributes() {
    return new DefaultErrorAttributes();
    }

    The class DefaultErrorAttributes contains the methods:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
    Map<String, Object> errorAttributes = this.getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
    if (this.includeException != null) {
    options = options.including(new Include[]{Include.EXCEPTION});
    }

    if (!options.isIncluded(Include.EXCEPTION)) {
    errorAttributes.remove("exception"); // information 1
    }

    if (!options.isIncluded(Include.STACK_TRACE)) {
    errorAttributes.remove("trace"); // information 2
    }

    if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
    errorAttributes.put("message", ""); // information 3
    }

    if (!options.isIncluded(Include.BINDING_ERRORS)) {
    errorAttributes.remove("errors"); // information 4
    }

    return errorAttributes;
    }

    private void addStatus(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) {
    Integer status = (Integer)this.getAttribute(requestAttributes, "javax.servlet.error.status_code");
    if (status == null) {
    errorAttributes.put("status", 999);
    errorAttributes.put("error", "None");
    } else {
    errorAttributes.put("status", status); // information 5

    try {
    errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());
    } catch (Exception var5) {
    errorAttributes.put("error", "Http Status " + status);
    }

    }
    }

    where those lines commented with information are shared, meaning that you can get those information as a variable in your template engine. For example, to get the error code inline, you could do:

    1
    2
    3
    <p>
    status:[[${status}]]
    </p>

Customizing your Error Pages with HTML

  • If you are using a template engine, then your error pages should go into the directory templates/error/<error-code>.html. (Or the 4xx.html or 5xx.html file)

    For example, if you want to have a customized response to 404 error, you should place your 404.html to templates/error/404.html.

  • If you are not using a template engine, or that your template engine cannot find that specific error page, then the error request will map to pages stored under your static folder.

    For example, if you want to have a customized response to 404 error, you should place your 404.html to static/error/404.html.

  • If both the above cannot be satisfied, Spring Boot will start its default mechanism of resolving and constructing a White Labeled Page.

Customizing your Error Pages with JSON

In summary, you need to do:

  1. Create your own Exception class, which can contain information such as error message.
  2. Create a @ControllerAdvice class, which is a central place for handling/customizing your @ExceptionHandler(<YourException>.class)
  3. Throw that Exception class in your controller when needed. That xxxException will first go to that @ExceptionHandler(xxxException.class) you created in the step above, and if not found, it will take its default JSON format output.

Example: ALWAYS Returning Customized JSON

The basic step follows the summary above, but when you specified @ExceptionHandler, you also specify @ResponceBody.

For example, first you can create an Exception class called RequestNotFoundException:

1
2
3
4
5
6
// located in your project, not in those static folders of course
public class RequestNotFoundException extends RuntimeException{
public RequestNotFoundException() {
super("Request Not Found");
}
}

Then, this could be your @ControllerAdvice for handling the exceptions you throw:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@ControllerAdvice
public class MyExceptionHandler {

@ResponseBody // return as a JSON in html. However, this has a side-effect, which will be addressed later
@ExceptionHandler(RequestNotFoundException.class) // if this Error happens, then this will be called
public Map<String, Object> handleException(RequestNotFoundException e){
Map<String, Object> error = new HashMap<>(); // can be an object as well
error.put("errorCode", "599");
error.put("message","Request Not Found Error");

return error;
}

@ResponseBody // return as a JSON in html
@ExceptionHandler(Exception.class) // if other error happens, this will be called
public Map<String, Object> handleGenericException(){
Map<String, Object> error = new HashMap<>(); // can be an object as well
error.put("errorCode", "569");
error.put("message","Generic Error");

return error;
}
}

Lastly, if you throw this RequestNotFoundException in your program, for example:

1
2
3
4
5
6
7
8
@ResponseBody
@GetMapping("/hello-response")
public String helloResponce(@RequestParam("req") String request){
if(request.equals("HAHA")){
throw new RequestNotFoundException();
}
return "Halo";
}

Now, if you try: localhost:8080/hello-response?req=HAHA, you will go to a page with the json data you specified in your @ExceptionHandler() method:

1
2
errorCode: "599"
message: "Request Not Found Error"

However, this has a side-effect:

  • Since you have specified @ResponceBody, no matter what you use (browser or other client applications) you will get a json formatted output, instead of the automatic resolution to html page when handled to a browser.

Example: Reproducing HTML for Browser

If you want the behavior that Spring Boot had. namely: provides a html page when requested by a browser, but a json page when requested by other client side applications, you need to tweak your @ExceptionHandler a bit.

For example, if you had the same RequestNotFoundException class, your @ExceptionHandler would look like:

1
2
3
4
5
6
7
8
9
10
11
12
@ControllerAdvice
public class MyExceptionHandler {

@ExceptionHandler(RequestNotFoundException.class) // notice that @ResponceBody is removed
public String handleException(RequestNotFoundException e){
Map<String, Object> error = new HashMap<>();
error.put("errorCode", "599");
error.put("message","Request Not Found Error");

return "forward:/error"; // notice the foward here
}
}

Now, when the exception RequestNotFound occurs, it will be forwards to the /error request. Remember, the /error request is handled well by Spring Boot.

However, if you run your program at this point, you will encounter a problem: although the behavior of receiving an html page has been reproduced, you received a White Labeled Page with an error code of 200. This is caused by the fact that your exception cannot be resolved successfully by your ExceptionResolver.

To fix this issue, we need to pass in the error code in the Spring Boot way. If we look at the class AbstractErrorController which is used by BasicErrorController, we see the method:

1
2
3
4
5
6
7
8
9
10
11
12
protected HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); // notice this line
if (statusCode == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
try {
return HttpStatus.valueOf(statusCode);
}
catch (Exception ex) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}

That ERROR_STATUS_CODE comes fromString ERROR_STATUS_CODE = "javax.servlet.error.status_code". Therefore, to pass in the error code in the Spring Boot way, you need to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ControllerAdvice
public class MyExceptionHandler {

@ExceptionHandler(RequestNotFoundException.class)
public String handleException(RequestNotFoundException e, HttpServletRequest request){ // changed here
request.setAttribute("javax.servlet.error.status_code",599); // changed here
request.setAttribute("javax.servlet.error.message","Request Not Found"); // changed here

Map<String, Object> error = new HashMap<>(); // side-effect here
error.put("errorCode", "599");
error.put("message","Request Not Found Error");

return "forward:/error";
}
}

Now, the Spring Boot default resolver will be able to resolve this exception correctly, so that if the exception is raised in a browser, a html page will be shown; in other client applications, data in json format will be shown.

Yet there is still a side-effect, namely your customized information in error object will not be shown, since it is not used at all!

Example: Reproducing HTML for Browser while Keeping all Customized Data

If you want to have the behavior of keeping your customized data (the ones stored in the error object above), and have the behavior of returning a html/json page based on whether a browser/other client applications are used, you have two choice:

  1. You can implement your own ErrorController class, extending from the AbstractErrorController (the easier way) or implementing the ErrorController interface (the harder way).

    This works because they key is to change the two methods noted below (which determines the returned result):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class BasicErrorController extends AbstractErrorController {

    private final ErrorProperties errorProperties;

    /**
    * Create a new {@link BasicErrorController} instance.
    * @param errorAttributes the error attributes
    * @param errorProperties configuration properties
    */
    public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
    this(errorAttributes, errorProperties, Collections.emptyList());
    }

    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) // this method
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
    HttpStatus status = getStatus(request);
    Map<String, Object> model = Collections
    .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
    response.setStatus(status.value());
    ModelAndView modelAndView = resolveErrorView(request, response, status, model);
    return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
    }

    @RequestMapping // and this method
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
    HttpStatus status = getStatus(request);
    if (status == HttpStatus.NO_CONTENT) {
    return new ResponseEntity<>(status);
    }
    Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
    return new ResponseEntity<>(body, status);
    }

    and this BasicErrorController is only injected in the container if (inside the ErrorMvcAutoConfiguration class):

    1
    2
    3
    4
    5
    6
    7
    @Bean
    @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT) // notice here
    public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
    ObjectProvider<ErrorViewResolver> errorViewResolvers) {
    return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
    errorViewResolvers.orderedStream().collect(Collectors.toList()));
    }

    Therefore, if you implemented your own AbstractErrorController abstract class or the ErrorController interface and overrides the two methods noted above, your should get your desired behavior.

  2. However, if we look closer at the two methods we need to change, we can find an easier way to achieve the same goal.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
    HttpStatus status = getStatus(request);
    // both methods uses the getErrorAttributes() method to contruct the final return result
    Map<String, Object> model = Collections
    .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
    response.setStatus(status.value());
    ModelAndView modelAndView = resolveErrorView(request, response, status, model);
    return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
    }

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
    HttpStatus status = getStatus(request);
    if (status == HttpStatus.NO_CONTENT) {
    return new ResponseEntity<>(status);
    }
    // both methods uses the getErrorAttributes() method to contruct the final return result
    Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
    return new ResponseEntity<>(body, status);
    }

    Now, if we look at that getErrorAttributes method, we see:

    1
    2
    3
    4
    protected Map<String, Object> getErrorAttributes(HttpServletRequest request, ErrorAttributeOptions options) {
    WebRequest webRequest = new ServletWebRequest(request);
    return this.errorAttributes.getErrorAttributes(webRequest, options);
    }

    and that ErrorAttribute also has a default implementation injected, namely:

    1
    2
    3
    4
    5
    @Bean
    @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
    public DefaultErrorAttributes errorAttributes() {
    return new DefaultErrorAttributes();
    }

    Therefore, we could just implement our own ErrorAttributes class, which has to satisfy the following, or extending that DefaultErrorAttribute class while overriding its getErrorAttributes() method (which is easier):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public interface ErrorAttributes {
    /** @deprecated */
    @Deprecated
    default Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
    return Collections.emptyMap();
    }

    default Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
    return this.getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
    }

    Throwable getError(WebRequest webRequest);
    }

    Here, we will simply extend that DefaultErrorAttribute class:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Component
    public class MyErrorAttributes extends DefaultErrorAttributes {
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {

    Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
    errorAttributes.put("new_thing","default_value_here");

    // this receives the error object
    Map<String, Object> extension = (Map<String, Object>) webRequest.getAttribute("extension", 0); // see later
    errorAttributes.put("extension", extension);

    // you can also do this, if you are sure that there is no name collision
    /*errorAttributes.putAll(extension);*/

    return errorAttributes;
    }
    }

    And your @ExceptionHandler looks like:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @ControllerAdvice
    public class MyExceptionHandler {

    @ExceptionHandler(RequestNotFoundException.class)
    public String handleException(RequestNotFoundException e, HttpServletRequest request){
    request.setAttribute("javax.servlet.error.status_code",509);
    request.setAttribute("javax.servlet.error.message","Request Not Found");


    Map<String, Object> error = new HashMap<>();
    error.put("errorCode", "599");
    error.put("message","Request Not Found Error");

    request.setAttribute("extension",error); // so that our ErrorAttribute class above can access it

    return "forward:/error";
    }
    }

    Now, if you revisit your site, everything would work correctly. This would be the ultimate solution.

    Note:

    • We see that webRequest.getAttribute("extension", 0) needs two parameters. The second parameter actually specifies which scope to get that attribute object. For more details, look that that getAttribute() method:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public interface RequestAttributes {
      int SCOPE_REQUEST = 0;
      int SCOPE_SESSION = 1;
      String REFERENCE_REQUEST = "request";
      String REFERENCE_SESSION = "session";

      @Nullable
      Object getAttribute(String var1, int var2);
      ...
      }

Caching in Spring Boot

Topics addressed here includes the JSR-107 standard, Spring Cache Abstraction, and redis inclusion in Spring Boot.

Caching is used widely for web applications, as it can reduce the number of round-trips when a query has been made. This is achieved by storing data temporarily in cache, which then can be retrieved by clients immediately (before expiration) since it is now stored locally. This also has the further advantage of enjoying the hardware spec of the client instead of letting the server/database do all those work :). As a result, this can speed up your application and its performance.

JSR107/JCache

JCache (aka JSR-107) specification defines the standard caching API for Java. These include:

  • CachingProvider
    • This API is used to create/configure/manage multiple CacheManager. There can be multiple instance of this in an application.
  • CacheManager
    • This API is used to create/configure/manage multiple (unique) Cache instances. There can be multiple CacheManager controller by a CachingProvider, but a CacheManager can only be controlled by one CachingProvider.
  • Cache
    • This API is used to store Entries. It can be thought as a HashMap data structure, which stores Entry that is basically a key-value pair.
  • Entry
    • This API is basically a key-value pair of data being stored in the Cache. The same Entry can be stored in multiple different Cache as well.
  • Expiry
    • This API controls the expiration time of a cache Entry. This is also stored in a Cache. Once the expiration time passed, that Entry can no longer be accessed, updated or deleted.

A graphical representation of the above would be:

jsr107-graph.png

Importing JSR107 API

First, you will need to import the dependency:

1
2
3
4
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>

Now, if you look at the jar package imported, you will find the five API mentioned above:

jsr107-jar.png

For example, in that CacheProvider class, you will see methods such as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface CachingProvider extends Closeable {
CacheManager getCacheManager(URI var1, ClassLoader var2, Properties var3); // notice here

ClassLoader getDefaultClassLoader();

URI getDefaultURI();

Properties getDefaultProperties();

CacheManager getCacheManager(URI var1, ClassLoader var2); // here

CacheManager getCacheManager(); // and here

void close();

void close(ClassLoader var1);

void close(URI var1, ClassLoader var2);

boolean isSupported(OptionalFeature var1);
}

and in CacheManager interface, you will find:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface CacheManager extends Closeable {
CachingProvider getCachingProvider();

...

<K, V> Cache<K, V> getCache(String var1, Class<K> var2, Class<V> var3); // notice here

<K, V> Cache<K, V> getCache(String var1); // and here

Iterable<String> getCacheNames();

void destroyCache(String var1);

void enableManagement(String var1, boolean var2);

void enableStatistics(String var1, boolean var2);

void close();

boolean isClosed();

<T> T unwrap(Class<T> var1);
}

Then the other three classes are also straight forward enough so that they are not shown here.

However, a problem with using JSR107 is that not all implementations we use later implements JSR107. This means that if we use JSR107 APIs, we might need to implement some of the methods by ourselves for things to work correctly.

Luckily, Spring also has a solution to this by providing its own APIs, the Spring Cache Abstraction, which also allows us to use annotations (also available in JSR107).

Spring Cache Abstraction

Spring simplifies the JSR107 APIs by using only CacheManager and Cache as the central APIs (other three APIs are also implemented, but not meant for us to use directly), as well as some annotations (some from JSR-107, some made by Spring).

API/Annotation Usage
Cache API that defines all the caching operation (e.g. CRUD). Spring has some default implementations including RedisCache, EhCacheCache, ConcurrentMapCache, etc.
CacheManager API that creates and manages Cache.
@Cacheable Mainly used on reading methods. This will cache the argument and result pair of a method call. After being cached, this method with the same argument called will not be executed anymore.
@CacheEvict Mainly used on deleting methods. This will delete a specific result from the cache.
@CachePut Mainly used on updating methods. This will cache/update the argument and result pair in the Cache. The method will always execute even it has been cached.
@EnableCaching Enables the caching behavior with annotation described above.
KeyGenerator Defines how the key data is stored in a Cache for later look ups.
serialize Defines how the value data is stored in a Cache (e.g. when you are storing an object).

Note:

  • Whenever you use @Cacheable, @CachePut, and @CacheEvict together, you need to make sure that they are manipulating entry with the same key!

Importing Spring Cache

To import Spring Cache Abstraction, add the following dependency to your pom.xml:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

Using Spring Cache

First, you need to have some sort of database access setup, and some query operations implemented (assuming you want to cache those).

Now, to enable caching and start using caching operations, you need to do the following steps:

  1. To use annotation based caching operations, add @EnableCaching to your application.
  2. Use annotation @Cacheable, @CacheEvict, etc. mentioned in the table above.
    1. When you specify @Cacheable (for example), you can/should also specify some of the options, including cacheName, key/keyGenerator, cacheManager, and etc.

A simple example would be:

Your main application class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.testmybatis;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@MapperScan("com.example.testmybatis.mappers")
@EnableCaching // notice here
public class TestMybatisApplication {

public static void main(String[] args) {
SpringApplication.run(TestMybatisApplication.class, args);
}

}

One of your controller class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.testmybatis.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class testCachingController {
Logger logger = LoggerFactory.getLogger(getClass());

@Cacheable(cacheNames = "test") // notice here
@RequestMapping("/cache/calc/{num}")
public Integer multiplyByTwo(@PathVariable(name = "num") Integer num){
logger.info("Entered Cacheable Method");
return num*2;
}
}

Now, the first time you go to localhost:8080/cache/calc/2, this method will be run and you will see the logger info Entered Cacheable Method. However, if you resend the same request by refreshing it, this method will no longer be entered yet the result will be returned (from cache).

Some of the options that you can specify with @Cacheable include:

  • cacheName orvalue
    • This sets the name of the cache(s) that this data/entry will go to (see picture below)
  • key or keyGenerator
    • This sets the key of the entry that will be stored in cache (by default it uses the method argument)
    • Some SpEL can be used
      • e.g. @Cacheable(key="#root.methodName + '[' + #a0 + ']'")
  • cacheManager or cacheResolver
    • This sets the cacheManager that its corresponding cache should point to
  • condition
    • This sets the condition of when to cache the entry. Entry will be cached if this condition returns true
      • e.g. @Cacheable(condition = "#a0 eq 'aaaa' and #a1 > 0")
  • unless
    • This sets the condition of when to cache the entry. Entry will not be cached if this condition returns true

Picture taken from www.atguigu.com

For more details on each of the options mentioned above, you can look into the class @Cacheable.

Some of the SpEL you can use in those options:

Name Location Description Example
methodName root object The name of the method being invoked #root.methodName
method root object The method being invoked #root.method.name
target root object The target object being invoked #root.target
targetClass root object The class of the target being invoked #root.targetClass
args root object The arguments (as array) used for invoking the target #root.args[0]
caches root object Collection of caches against which the current method is executed #root.caches[0].name
argument name evaluation context Name of any of the method argument. If for some reason the names are not available (ex: no debug information), the argument names are also available under the a<#arg> where #arg stands for the argument index (starting from 0). ibanora0(one can also usep0or p<#arg> notation as an alias).
result evaluation context The result of the method call (the value to be cached). Only available in ‘unless‘ expressions and cache evict expression (when beforeInvocation is false). #result

(Table from https://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/cache.html, for information on other SpEL, you should also visit that link)

Spring Implementation

The first thing to do is to look at the xxxAutoConfiguration of course. If we look at the CacheAutoConfigurationclass:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(CacheManager.class)
@ConditionalOnBean(CacheAspectSupport.class)
@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
@EnableConfigurationProperties(CacheProperties.class)
@AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,
HibernateJpaAutoConfiguration.class, RedisAutoConfiguration.class })
@Import({ CacheConfigurationImportSelector.class, CacheManagerEntityManagerFactoryDependsOnPostProcessor.class })
public class CacheAutoConfiguration {

...

/**
* {@link ImportSelector} to add {@link CacheType} configuration classes.
*/
static class CacheConfigurationImportSelector implements ImportSelector {

@Override // this method imports all those CacheConfigurations
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
CacheType[] types = CacheType.values();
String[] imports = new String[types.length];
for (int i = 0; i < types.length; i++) {
imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
}
return imports;
}

}

}

If you run this with the debugger of IntelliJ, you will see the following Configurations imported:

1
2
3
4
5
6
7
8
9
10
0 = "org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration"
1 = "org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration"
2 = "org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration"
3 = "org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration"
4 = "org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration"
5 = "org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration"
6 = "org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration"
7 = "org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration"
8 = "org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration"
9 = "org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration"

To see which configurations will be used in the end, you can either look at the conditions on the class headers, or you can run your application with debug=true, from which you will see that only one Configuration is matched (assuming you were following the example made above):

1
2
3
SimpleCacheConfiguration matched:
- Cache org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration automatic cache type (CacheCondition)
- @ConditionalOnMissingBean (types: org.springframework.cache.CacheManager; SearchStrategy: all) did not find any beans (OnBeanCondition)

Now, if you look at the SimpleCacheConfiguration class, you will see that it uses a ConcurrentMapCacheManager:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class SimpleCacheConfiguration {

@Bean // notice here
ConcurrentMapCacheManager cacheManager(CacheProperties cacheProperties,
CacheManagerCustomizers cacheManagerCustomizers) {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
List<String> cacheNames = cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
cacheManager.setCacheNames(cacheNames);
}
return cacheManagerCustomizers.customize(cacheManager);
}

}

Further source codes will not be shown here, but in summary:

  • ConcurrentMapCacheManager stores its Cache objects in a ConcurrentMap<String, Cache> data structure, in which String will be cache name.

  • ConcurrentMapCacheManager creates its Cache from the method:

    1
    2
    3
    4
    protected Cache createConcurrentMapCache(String name) {
    SerializationDelegate actualSerialization = this.isStoreByValue() ? this.serialization : null;
    return new ConcurrentMapCache(name, new ConcurrentHashMap(256), this.isAllowNullValues(), actualSerialization);
    }

    This returns a ConcurrentMapCache, which would be the Cache data structure to store the cache

    • The ConcurrentMapCache stores its key-value pair entry with ConcurrentMap<Object, Object>, which means both its key and its value will be objects:
    1
    2
    3
    4
    5
    6
    7
    public class ConcurrentMapCache extends AbstractValueAdaptingCache {
    private final String name;
    private final ConcurrentMap<Object, Object> store;
    @Nullable
    private final SerializationDelegate serialization;
    ...
    }

Mechanism

What happens under the hood when a @Cacheable is encountered? For example, when a request is sent to the following handler:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class testCachingController {
Logger logger = LoggerFactory.getLogger(getClass());

// what happens when it reaches here?
@Cacheable(cacheNames = "test")
@RequestMapping("/cache/calc/{num}")
public Integer multiplyByTwo(@PathVariable(name = "num") Integer num){
logger.info("Entered Cacheable Method");
return num*2;
}
}
  1. First, before this method gets executed, it will first look for the cache name with test by using the method from ConcurrentCacheManager:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Nullable
    public Cache getCache(String name) { // this method
    Cache cache = (Cache)this.cacheMap.get(name);
    if (cache == null && this.dynamic) {
    synchronized(this.cacheMap) {
    cache = (Cache)this.cacheMap.get(name);
    if (cache == null) {
    cache = this.createConcurrentMapCache(name); // creates if cache does not exist
    this.cacheMap.put(name, cache);
    }
    }
    }

    return cache;
    }
  2. Next, with this Cache created (ConcurrentMapCache), it will then first look into that cache to see if that key exists (even if the cache has just been created). This is executed inside the ConcurrentMapCache class:

    1
    2
    3
    4
    @Nullable
    protected Object lookup(Object key) {
    return this.store.get(key);
    }

    Note:

    • Where does the key come from? After the method argument being passed in and before this step, there is a generateKey() method which generates the key based on a KeyGenerator. By default, it uses SimpleKeyGenerator.
  3. If this lookup() did not find the key, then the method in the controller will be called. If found, returns the result.

  4. If the controller continued its execution, then the returned result will not only be returned to the user, but also stored in the cache with the method in the ConcurrentMapCache class:

    1
    2
    3
    public void put(Object key, @Nullable Object value) {
    this.store.put(key, this.toStoreValue(value));
    }

Example: Using a Customized KeyGenerator

First you need to inject a bean of type KeyGenerator with a specific bean name:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.testmybatis.config;

import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.lang.reflect.Method;
import java.util.Arrays;

@Configuration
public class CacheConfig {

@Bean(value = "myKeyGenerator") // this will be used to identify which generator to use
// @Qualifier(value = "myKeyGenerator") this is equivalent as above
public KeyGenerator keyGenerator(){
return new KeyGenerator() {
@Override
public Object generate(Object o, Method method, Object... objects) {
return method.getName() + Arrays.asList(objects).toString();
}
};
}
}

Now, to use this KeyGenerator, you just need to refer to this bean name:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.testmybatis.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class testCachingController {
Logger logger = LoggerFactory.getLogger(getClass());

@Cacheable(cacheNames = "test", keyGenerator = "myKeyGenerator") // referred here
@RequestMapping("/cache/calc/{num}")
public Integer multiplyByTwo(@PathVariable(name = "num") Integer num){
logger.info("Entered Cacheable Method");
return num*2;
}
}

Example: Using @Caching

@Caching has not been mentioned before, but this is simply a collection of @Cacheable, @CachePut, and @CacheEvict annotation. Using @Caching allows us to combine complicated behavior of manipulating entries in a cache.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.springframework.cache.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {
Cacheable[] cacheable() default {};

CachePut[] put() default {};

CacheEvict[] evict() default {};
}

An example usage would be:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.example.testmybatis.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class testCachingController {
Logger logger = LoggerFactory.getLogger(getClass());

@Caching(
cacheable = {
@Cacheable(cacheNames = "test", keyGenerator = "myKeyGenerator")
},
put = {
@CachePut(cacheNames = "test") // for the sake of an example
},
evict = {
@CacheEvict(cacheNames = "test") // for the sake of an example
}
)
@RequestMapping("/cache/calc/{num}")
public Integer multiplyByTwo(@PathVariable(name = "num") Integer num){
logger.info("Entered Cacheable Method");
return num*2;
}
}

Note:

  • Notice that there is a conflict between using @Cacheable and @CachePut, namely that if @Cacheable does not always invoke the method yet @CachePut always needs the method to execute. The effect in the end would be @CachePut overriding @Cacheable, such that even if the key is found in the cache, the method will still execute.

Using @CacheConfig

Notice that all of the above annotations have to specify cacheName= "test". Is there a way to centralize those common configurations altogether to save the typing? This can be achieved with @CacheConfig annotation with some specified properties at the class level.

The @CacheConfig class looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.springframework.cache.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheConfig {
String[] cacheNames() default {};

String keyGenerator() default "";

String cacheManager() default "";

String cacheResolver() default "";
}

This means that you can specify four common properties for all the cache annotations in the same class. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.testmybatis.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.*;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@CacheConfig(cacheNames = "test") // centralizing some cache properties
@RestController
public class testCachingController {
Logger logger = LoggerFactory.getLogger(getClass());

@Cacheable(keyGenerator = "myKeyGenerator") // will be saved to cache with name "test"
@RequestMapping("/cache/calc/{num}")
public Integer multiplyByTwo(@PathVariable(name = "num") Integer num){
logger.info("Entered Cacheable Method");
return num*2;
}
}

Using Redis in Spring Cache

By default, Spring uses SimpleCacheConfiguration, which stores cache data essentially in ConcurrentMap<Object, Object> data structure. However, what if we want to use Redis as our caching mechanism? To do this, we need to make sure that RedisCacheConfiguration is imported and enabled in our project.

Importing Redis

If you look at RedisCacheConfiguration class, you will see the following class header:

1
2
3
4
5
6
7
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisConnectionFactory.class) // this means we absolutely need the class RedisConnectionFactory
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {

Therefore, the logical step would be importing the Redis dependency related to Spring:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Since this is related to Spring Boot Starter, it will use the RedisAutoConfiguration class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
// these classes implements RedisConnectionFactory
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
// notice here
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

@Bean
@ConditionalOnMissingBean
// and this template
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

}

Notice that we will have RedisTemplate<Object, Object> injected as bean into our project (remember the JDBCTemplate that we used before), which we will touch on shortly.

Configuring your Redis Connection

This is even simpler than configuring database connection: all you need to do is to specify your Redis host IP and port (by default 6379):

1
2
3
4
spring:
redis:
host: 192.168.217.100
port: 6379

Once the connection is established, all you need to do is to use either RedisTemplate<Object, Object>, or StringRedisTemplate.

Using your Redis Templates

There are two templates injected into your container:

  • RedisTemplate<Object, Object>
    • This is used mainly when you need to write commands operating with Java objects
  • StringRedisTemplate
    • This is used mainly when you just need to write simple string commands

Similar to JDBCTemplate, you just need to Autowire either or both of the templates. For example:

1
2
3
4
5
@Autowired
StringRedisTemplate stringRedisTemplate;

@Autowired
RedisTemplate<Object, Object> redisTemplate;

In general, StringRedisTemplate would be simpler to use. To operate with the data structures in Redis with StringRedisTemplate, all you need to do is to call the method opsForXXX():

Method Name Redis Operation
opsForValue() Operations with String in Redis
opsForSet() Operations with Set in Redis
opsForList() Operations with List in Redis
opsForHash() Operations with Hash in Redis
opsForZSet() Operations with Zset in Redis

For example:

1
2
3
4
5
6
7
8
9
10
11
@Autowired
StringRedisTemplate stringRedisTemplate;

@Autowired
RedisTemplate<Object, Object> redisTemplate;

@Test
public void test(){
// gets the key-value pair where key="msg"
System.out.println(stringRedisTemplate.opsForValue().get("msg"));
}

Introduction to Distributed Computing

In the manual above, we were using the MVC (Model-View-Controller) architecture. This works fine, but problem occurs when request to a certain service increases, which would cause heavy server load and hence performance issues. Therefore, distributed model has been promoted when needs for a service are high.

https://imgs.developpaper.com/imgs/2266318593-22ea4454ae5d1cfc_articlex.png

Basic Mechanism

The central question for distributed computing is: how can a client/remote server access services from other remote servers? The simplest model involves using a registry - a place where all the information of all the provided services is kept.

So in general, the mechanism is:

  1. A provider provides its services/implementations to a registry
  2. A client subscribes and retrieves a list of available services
  3. A client uses the service by telling registry to invoke/call methods from the provider

https://www.baeldung.com/wp-content/uploads/2017/12/dubbo-architecture-1024x639.png

(Dubbo is the tool for providing all those RPC - remote procedure call - mechanism, while ZooKeeper could be the registry)

Using Spring Cloud for Distributed Computing

While you could use Dubbo and ZooKeeper to establish a distributed computing system for your application, Spring Cloud also provides a well-rounded solution for distributing services.

In summary, to setup the simple system mentioned above, you would need to:

  1. Create a Eureka Server/Registry
    1. Include necessary dependencies (see below)
    2. Configure Eureka server settings in application.yml
    3. Use @EnableEurekaServer and start the application to start the server/registry
  2. Create a Service Provider
    1. Include necessary dependencies (see below)
    2. Configure Eureka server settings in application.yml for connection
    3. Create (a) RestController(s) to provide certain service(s)
    4. This would look like a normal Spring Web application, except for the second step
  3. Create a Consumer/User
    1. Also need to include Spring Cloud related dependencies (see below)
    2. Configure Eureka server settings in application.yml for connection
    3. Use @EnableDiscoveryClient annotation to make registered services available
    4. Inject a bean RestTemplate which would be able to invoke service calls and pass its result to us
    5. Create a controller (or anything) to use that RestTemplate

Example: Starting a Eureka Server/Registry

First, the necessary dependencies include:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

Then, your configuration for your Eureka server could be:

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 8765

eureka:
instance:
hostname: eureka-server

client:
register-with-eureka: false # does not register the server itself into the eureka registry
fetch-registry: false # does not fetch service registration from eureka registry
service-url:
defaultZone: http://localhost:8765/eureka/ # address of eureka registry

Finally, to start your Eureka server, you would use the annotation @EnableEurekaServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.testeurekaserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* Registry
*/
@EnableEurekaServer
@SpringBootApplication
public class TestEurekaServerApplication {

public static void main(String[] args) {
SpringApplication.run(TestEurekaServerApplication.class, args);
}

}

Now, if you visit localhost:8765, you will see the page for viewing current Eureka server’s status.

Example: Registering Services to Eureka Server

First, you would need the following dependencies:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>


<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

Then, your application.yml would look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 8001 # this now becomes useful with the below combination

spring:
application:
name: provider-ticket # this name will the the service name registered in Eureka Server

eureka:
instance:
prefer-ip-address: true # use service's IP to register as a service

client:
service-url:
defaultZone: http://localhost:8765/eureka/ # address of eureka registry

By setting eureka.instance.prefer-ip-address=true, it enables us to easily register the same service over several machines. This will be shown slightly later.

Now, you can create some simple service. For example:

1
2
3
4
5
6
7
8
package com.example.testproviderticket.service;

public class TicketServiceImpl implements TicketService{
@Override
public String getTicket() {
return "Ticket given";
}
}

And a controller for that service (Eureka uses HTTP for data transport):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.testproviderticket.controller;

import com.example.testproviderticket.service.TicketService;

@RestController
public class TicketController {

@Autowire
TicketService ticketService;

@GetMapping("/ticket")
public String getTicket(){
return ticketService.getTicket();
}
}

Now, if you run localhost:8001/ticket, you will see the normal result as if it is a simple Spring Web Application. However, if you now look at localhost:8765 (the address for Eureka registry), you will see the newly registered service.

Note:

  • To register/use services in Eureka Server, you need to make sure that the server is running.

Lastly, if you want to register the same service for multiple times, you can simply change the server.port to a different number, package the service application you created in this example as a jar, and run it. Now, if you go to Eureka server, you will see that the same service will now be provided in multiple ports! (This means you could easily deploy and register a service on multiple remotes).

Example: Creating a Consumer/User for Using Registered Services

First, you need to include dependencies such as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

Then, your Eureka server configuration would look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 8010

spring:
application:
name: consumer-user # this name will also be displayed at the same place as registered services on Eureka registry

eureka:
instance:
prefer-ip-address: true # use service's IP to register as a service

client:
service-url:
defaultZone: http://localhost:8765/eureka/ # address of eureka registry

Now, in order to discover and use services there, you need to have:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.testconsumeruser;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* Consumer
*/
@EnableDiscoveryClient // enable the functionality of discovering services in Eureka
@SpringBootApplication
public class TestConsumerUserApplication {

public static void main(String[] args) {
SpringApplication.run(TestConsumerUserApplication.class, args);
}

// This is used to get/use services from Eureka Server
@LoadBalanced // enable Load Balancing
@Bean // inject this into the application
public RestTemplate restTemplate(){
return new RestTemplate();
}

}

Lastly, to use registered services, you just need to have a controller (for example) using that RestTemplate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.testconsumeruser.controller;

@RestController
public class UserController {

@Autowire
RestTemplate restTemplate;

@GetMapping("/buy")
public String buyTicket(String name){
// http:<Provider-Application-Name>/<Specific-Service-Request>
// convert it to String.class
String serviceResult = restTemplate.getForObject("http://PROVIDER_TICKET/ticket", String.class);
return name+serviceResult;
}
}

Now, if you go to localhost:8010/buy?name=test, you will see that everything works that it invokes services/methods from other applications registered on Eureka server.