Backend Development 11 min read

Seven Common Spring Boot Performance Optimization Techniques

This article presents seven practical Spring Boot performance optimization methods—including asynchronous execution, increasing Tomcat connection limits, component scanning, switching to Undertow, buffered I/O, DeferredResult handling, and AsyncHandlerInterceptor usage—each illustrated with clear code examples and explanations.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Seven Common Spring Boot Performance Optimization Techniques

Hello everyone, I am Chen. This article introduces seven common Spring Boot performance optimization directions.

1. Asynchronous Execution

Two implementation methods are provided:

Use the asynchronous annotation @Async and add @EnableAsync to the startup class.

Leverage JDK 8's CompletableFuture class.

Example using a custom thread with CompletableFuture :

@AllArgsConstructor
public class AskThread implements Runnable {
    private CompletableFuture
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
future = new CompletableFuture<>();
        new Thread(new AskThread(future)).start();
        Thread.sleep(1000);
        future.complete(60);
    }
}

In this example, the AskThread starts before its data is ready, causing the call to myRe = re.get() * re.get() to block. After a simulated 1‑second delay, the result is supplied to the future , allowing the thread to continue.

Another example demonstrates CompletableFuture.supplyAsync to run a slow calc() method in a new thread while immediately returning a CompletableFuture that later provides the result:

public class Calc {
    public static Integer calc(Integer para) {
        try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
        return para * para;
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        final CompletableFuture
future = CompletableFuture.supplyAsync(() -> calc(50))
            .thenApply(i -> Integer.toString(i))
            .thenApply(str -> "\"" + str + "\"")
            .thenAccept(System.out::println);
        future.get();
    }
}

The supplyAsync method creates a CompletableFuture that runs calc() in a new thread, returning immediately. The future acts as a contract for obtaining the eventual result.

When a controller method returns a Callable , Spring MVC dispatches it to a TaskExecutor for asynchronous processing, keeping the response open until the callable completes.

@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(); }
    @GetMapping("/hello")
    public Callable
helloController() {
        logger.info(Thread.currentThread().getName() + " entering helloController");
        Callable
callable = new Callable
() {
            @Override public String call() throws Exception {
                logger.info(Thread.currentThread().getName() + " entering call");
                String say = hello.sayHello();
                logger.info(Thread.currentThread().getName() + " from helloService");
                return say;
            }
        };
        logger.info(Thread.currentThread().getName() + " returning from helloController");
        return callable;
    }
}

Asynchronous calls can also be handled with WebAsyncTask , allowing custom timeout handling:

@RestController
public class HelloController {
    private static final Logger logger = LoggerFactory.getLogger(HelloController.class);
    @Autowired private HelloService hello;
    @GetMapping("/world")
    public WebAsyncTask
worldController() {
        logger.info(Thread.currentThread().getName() + " entering helloController");
        WebAsyncTask
webAsyncTask = new WebAsyncTask<>(3000, new Callable
() {
            @Override public String call() throws Exception {
                logger.info(Thread.currentThread().getName() + " entering call");
                String say = hello.sayHello();
                logger.info(Thread.currentThread().getName() + " from helloService");
                return say;
            }
        });
        webAsyncTask.onCompletion(() -> logger.info(Thread.currentThread().getName() + " completed"));
        webAsyncTask.onTimeout(() -> { logger.info(Thread.currentThread().getName() + " onTimeout"); throw new TimeoutException("call timeout"); });
        return webAsyncTask;
    }
    @GetMapping("/exception")
    public WebAsyncTask
exceptionController() {
        Callable
callable = () -> { logger.info(Thread.currentThread().getName() + " entering call"); throw new TimeoutException("call timeout!"); };
        return new WebAsyncTask<>(20000, callable);
    }
}

2. Increase Embedded Tomcat Maximum Connections

Configuration example to raise connection and thread limits:

@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();
            protocol.setMaxConnections(20000);
            protocol.setMaxThreads(2000);
            protocol.setConnectionTimeout(30000);
        }
    }
}

3. Use @ComponentScan()

Applying @ComponentScan() can scan packages faster than the default @SpringBootApplication annotation.

4. Switch Default Tomcat Container to Undertow

Replace the Tomcat starter with Undertow in Maven:

<exclusions>
    <exclusion>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
    </exclusion>
</exclusions>

Change to:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

5. Use BufferedWriter for Buffering

Encourage using BufferedWriter to improve I/O efficiency (code omitted for brevity).

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
executeSlowTask() {
        logger.info(Thread.currentThread().getName() + " entering executeSlowTask");
        DeferredResult
deferredResult = new DeferredResult<>();
        taskService.execute(deferredResult);
        logger.info(Thread.currentThread().getName() + " returning from executeSlowTask");
        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 {
        String resp = "my name is chhliu!";
        response.setContentLength(resp.length());
        response.getOutputStream().write(resp.getBytes());
        logger.info(Thread.currentThread().getName() + " entering afterConcurrentHandlingStarted");
    }
}

Finally, the article invites readers to join a backend‑focused technical community for knowledge sharing and networking.

performance optimizationSpring Bootthread poolTomcatUndertowAsynchronous ExecutionDeferredResult
Code Ape Tech Column
Written by

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

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.