For creating Java web apps and micro-services, Spring Boot is a popular and preffered framework. Although Spring Boot offers a plethora of built-in functionality, performance optimisation is essential for managing heavy traffic and enhancing user experience. We will look at many ways to improve the speed of your Spring Boot applications in this blog article, such as class data sharing, thread configuration in controllers, and database access layer thread pool setups.
Class Data Sharing (CDS)
The Java Virtual Machine (JVM) has a feature called Class Data Sharing (CDS) that facilitates the sharing of class metadata among many JVM processes. This speeds up startup times and lowers the memory footprint. To enable CDS, we can simply follow the below steps to do the same:
- Generate a Class List: Use the
-Xshare:off
and-XX:DumpLoadedClassList
options to generate a list of classes loaded by your application.
java -Xshare:off -XX:DumpLoadedClassList=classes.lst -jar your-spring-boot-app.jar
- Create a Shared Archive: Use the generated class list to create a shared archive with the
-Xshare:dump
option.
java -Xshare:dump -XX:SharedClassListFile=classes.lst -XX:SharedArchiveFile=app-cds.jsa
- Use the Shared Archive: Start your application with the
-XX:SharedArchiveFile
option.
java -Xshare:on -XX:SharedArchiveFile=app-cds.jsa -jar your-spring-boot-app.jar
Thread Configuration in Controllers
Configuring threads in controllers is critical for efficiently managing the coming concurrent requests. Spring Boot offers various thread configuration options, like the annotation @Async for asynchronous processing and thread pools. Let us look at an example on using the above annotation:
@SpringBootApplication
@EnableAsync
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
In the above code we see that we have a springboot application with async support enabled. Now let us create a service class which will have an asynchronous method. This will use the @Async annotation.
@Service
public class SampleService {
@Async
public CompletableFuture<String> processAsync() throws InterruptedException {
Thread.sleep(5000); // Simulate a long-running task
return CompletableFuture.completedFuture("Processed");
}
}
The method processAsync() will be an asynchronous method which will work on a different thread. In the above example we simulate a long running task with the thread sleep command.
Now in the controller we can autowire the class and call it’s method which will make it spin a new thread and run the task over there.
@Autowired
private MyService myService;
@GetMapping("/process")
public CompletableFuture<String> process() {
return myService.processAsync();
}
Alternatively, we can also configure a thread pool for asynchronous processing using a component.
@Configuration
@EnableAsync
public class AsynchronousConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
executor.initialize();
return executor;
}
}
Database Access Layer Thread Pool Configurations
Efficient database access is critical to the performance of any application. Configuring a connection pool can greatly enhance the speed of your Spring Boot application by reusing database connections and lowering the overhead associated with generating new ones.
One of the mostly used connection pool along with Spring Boot is HikariCP. We can configure it using our application.properties file.
spring.datasource.url=jdbc:mysql://localhost:3306/dbname
spring.datasource.username=username
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.max-lifetime=600000
This can also be achieved using a Configuration class as well:
@Configuration
public class DatabaseConfig {
@Bean
public Executor dbExec() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
executor.setThreadNamePrefix("DBExec-");
executor.initialize();
return executor;
}
}
Caching
Implement caching to reduce database load and increase response time. For caching methods, we can simply use the @Cacheable annotation. This implements a local caching mechanism. For distributed caching we use different tools (Redis, Hazelcast etc.) and logic.
@Service
public class MyService {
@Cacheable("items")
public Item getItemById(Long id) {
// Fetch item from database
return itemRepository.findById(id).orElse(null);
}
}
Batch Processing
Batch processing refers to the processing of huge amounts of data in chunks or batches, which can dramatically increase your application’s performance and resource utilisation. Spring boot comes with the pring-boot-starter-batch dependency. We can utilize this library to implement batch processing in our applications.
We work with batch jobs, we create a configuration class to configure a job
@Configuration
@EnableBatchProcessing
public class BatchConfig {
@Bean
public Job job(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory) {
Step step = stepBuilderFactory.get("step1")
.tasklet(tasklet())
.build();
return jobBuilderFactory.get("job")
.incrementer(new RunIdIncrementer())
.start(step)
.build();
}
@Bean
public Tasklet tasklet() {
return (contribution, chunkContext) -> {
System.out.println("Executing batch job");
return RepeatStatus.FINISHED;
};
}
}
We can run the job now, from wherever we want by autowiring the same.
@Component
public class BatchJobRunner implements CommandLineRunner {
@Autowired
private JobLauncher jobLauncher;
@Autowired
private Job job;
@Override
public void run(String... args) throws Exception {
jobLauncher.run(job, new JobParametersBuilder().toJobParameters());
}
}
Job and JobLauncher are the required to be imported from the spring boot’s batch library dependency.
Lazy Loading
Lazy loading basically delays object initialization until it is required. This can enhance application performance by minimising loading of data which are not required. We can use lazy loading for associations in the JPA entities to avoid unnecessary data fetching. Of course, this depends on the use case or the functionality we are trying to get.
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
}
Compression and Minification
When working with web based applications, the size of payload in request and response plays a huge role in the performance of the application. Compressing or minifying any payload boost the performance of any web applications. We can enable gzip compression for HTTP responses and minify static resources to reduce the size of any payload.
server.compression.enabled=true
server.compression.mime-types=text/html,text/xml,text/plain,text/css,text/javascript,application/javascript