Seven Common Spring Boot Performance Optimization Techniques
This article presents seven practical Spring Boot performance optimization methods—including asynchronous execution, increasing embedded Tomcat connection limits, component scanning, switching to Undertow, using BufferedWriter, DeferredResult, and AsyncHandlerInterceptor—each illustrated with code samples and explanations for improving Java backend applications.
Hello everyone, I'm Chen. This article introduces seven common Spring Boot performance optimization directions.
1. Asynchronous Execution
Two implementation ways:
Use the asynchronous annotation @Async and enable it with @EnableAsync on the main class.
Leverage JDK 8's CompletableFuture class.
@AllArgsConstructor
public class AskThread implements Runnable {
private CompletableFuture<Integer> re = null;
public void run() {
int myRe = 0;
try {
myRe = re.get() * re.get();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(myRe);
}
public static void main(String[] args) throws InterruptedException {
final CompletableFuture<Integer> future = new CompletableFuture<>();
new Thread(new AskThread(future)).start();
// Simulate a long‑running computation
Thread.sleep(1000);
// Notify completion
future.complete(60);
}
}In this example a thread is started; the AskThread object initially lacks the data it needs, so the expression myRe = re.get() * re.get() blocks until the future is completed.
After a simulated 1‑second delay, the result is set on the future, allowing the AskThread to continue.
public class Calc {
public static Integer calc(Integer para) {
try {
// Simulate a long‑running execution
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return para * para;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
final CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> calc(50))
.thenApply(i -> Integer.toString(i))
.thenApply(str -> "\"" + str + "\"")
.thenAccept(System.out::println);
future.get();
}
}The CompletableFuture.supplyAsync method creates a CompletableFuture that runs the supplied task in a new thread and returns immediately, providing a contract for obtaining the result later.
When a return value is needed, CompletableFuture is used; for fire‑and‑forget scenarios, runAsync(Runnable) is preferred, especially when optimizing controllers.
Both methods, if no custom thread pool is specified, execute in the ForkJoinPool.common daemon thread pool, which terminates when the main thread ends.
Core code example:
CompletableFuture.runAsync(() ->
this.afterBetProcessor(betRequest, betDetailResult, appUser, id)
);Asynchronous calls can also be implemented with Callable:
@RestController
public class HelloController {
private static final Logger logger = LoggerFactory.getLogger(HelloController.class);
@Autowired
private HelloService hello;
@GetMapping("/helloworld")
public String helloWorldController() {
return hello.sayHello();
}
/**
* Asynchronous RESTful call. When the controller returns a Callable, Spring MVC hands it to a TaskExecutor.
*/
@GetMapping("/hello")
public Callable<String> helloController() {
logger.info(Thread.currentThread().getName() + " entering helloController method");
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
logger.info(Thread.currentThread().getName() + " entering call method");
String say = hello.sayHello();
logger.info(Thread.currentThread().getName() + " returned from helloService method");
return say;
}
};
logger.info(Thread.currentThread().getName() + " returning from helloController method");
return callable;
}
}Using WebAsyncTask for asynchronous calls with timeout handling:
@RestController
public class HelloController {
private static final Logger logger = LoggerFactory.getLogger(HelloController.class);
@Autowired
private HelloService hello;
/** Timeout‑aware asynchronous request */
@GetMapping("/world")
public WebAsyncTask<String> worldController() {
logger.info(Thread.currentThread().getName() + " entering helloController method");
WebAsyncTask<String> webAsyncTask = new WebAsyncTask<>(3000, new Callable<String>() {
@Override
public String call() throws Exception {
logger.info(Thread.currentThread().getName() + " entering call method");
String say = hello.sayHello();
logger.info(Thread.currentThread().getName() + " returned from helloService method");
return say;
}
});
logger.info(Thread.currentThread().getName() + " returning from helloController method");
webAsyncTask.onCompletion(() -> logger.info(Thread.currentThread().getName() + " completed"));
webAsyncTask.onTimeout(() -> { throw new TimeoutException("call timeout"); });
return webAsyncTask;
}
/** Asynchronous call with exception handling */
@GetMapping("/exception")
public WebAsyncTask<String> exceptionController() {
logger.info(Thread.currentThread().getName() + " entering helloController method");
Callable<String> callable = () -> { throw new TimeoutException("call timeout!"); };
logger.info(Thread.currentThread().getName() + " returning from helloController method");
return new WebAsyncTask<>(20000, callable);
}
}2. Increase Embedded Tomcat Maximum Connections
Configuration example:
@Configuration
public class TomcatConfig {
@Bean
public ConfigurableServletWebServerFactory webServerFactory() {
TomcatServletWebServerFactory tomcatFactory = new TomcatServletWebServerFactory();
tomcatFactory.addConnectorCustomizers(new MyTomcatConnectorCustomizer());
tomcatFactory.setPort(8005);
tomcatFactory.setContextPath("/api-g");
return tomcatFactory;
}
class MyTomcatConnectorCustomizer implements TomcatConnectorCustomizer {
@Override
public void customize(Connector connector) {
Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
// Set maximum connections
protocol.setMaxConnections(20000);
// Set maximum threads
protocol.setMaxThreads(2000);
protocol.setConnectionTimeout(30000);
}
}
}3. Use @ComponentScan()
Applying @ComponentScan() can scan packages faster than using @SpringBootApplication alone.
4. Switch Default Tomcat Container to Undertow
Undertow offers higher throughput (e.g., 8000 vs. 5000 for Tomcat).
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>Replace with:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>5. Use BufferedWriter for Buffering
Readers can try this technique on their own; no code example is provided.
6. DeferredResult for Asynchronous Calls
@RestController
public class AsyncDeferredController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final LongTimeTask taskService;
@Autowired
public AsyncDeferredController(LongTimeTask taskService) {
this.taskService = taskService;
}
@GetMapping("/deferred")
public DeferredResult<String> executeSlowTask() {
logger.info(Thread.currentThread().getName() + " entering executeSlowTask method");
DeferredResult<String> deferredResult = new DeferredResult<>();
// Invoke long‑running task
taskService.execute(deferredResult);
logger.info(Thread.currentThread().getName() + " returning from executeSlowTask method");
deferredResult.onTimeout(() -> {
logger.info(Thread.currentThread().getName() + " onTimeout");
deferredResult.setErrorResult("time out!");
});
deferredResult.onCompletion(() -> logger.info(Thread.currentThread().getName() + " onCompletion"));
return deferredResult;
}
}7. AsyncHandlerInterceptor for Intercepting Asynchronous Calls
@Component
public class MyAsyncHandlerInterceptor implements AsyncHandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(MyAsyncHandlerInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
logger.info(Thread.currentThread().getName() + " service completed, returning result to client");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
if (ex != null) {
System.out.println("Exception occurred:" + ex.getMessage());
}
}
@Override
public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// Replace original response with custom content
String resp = "my name is chhliu!";
response.setContentLength(resp.length());
response.getOutputStream().write(resp.getBytes());
logger.info(Thread.currentThread().getName() + " entering afterConcurrentHandlingStarted method");
}
}The remainder of the original article contains promotional links and social media calls to action, which are omitted from this technical summary.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
