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<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();
        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<Void> 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<String> helloController() {
        logger.info(Thread.currentThread().getName() + " entering helloController");
        Callable<String> callable = new Callable<String>() {
            @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<String> worldController() {
        logger.info(Thread.currentThread().getName() + " entering helloController");
        WebAsyncTask<String> webAsyncTask = new WebAsyncTask<>(3000, new Callable<String>() {
            @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<String> exceptionController() {
        Callable<String> 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<String> executeSlowTask() {
        logger.info(Thread.currentThread().getName() + " entering executeSlowTask");
        DeferredResult<String> 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Spring Bootthread poolundertowDeferredResult
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

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.