Build a Custom Spring Boot Starter to Package Common Utilities for One-Click Integration
The article walks through creating a custom Spring Boot starter that bundles common utilities such as unified response wrapping, global exception handling, type converters, thread pools, login context, and data masking, showing the standard two‑module structure, Maven setup, configuration properties, conditional annotations, and usage in a business project.
Problem Statement
In daily development many teams repeatedly copy configuration classes, utility classes, and annotations for common capabilities such as unified response wrapping, global exception handling, custom type converters, thread pools, login context, data masking, and Redis utilities. This leads to duplicated work and inconsistent team standards.
Solution Overview
Spring Boot starters encapsulate reusable logic into independent components. By adding a starter dependency, projects obtain automatic configuration, zero‑configuration usage, and a unified technical standard.
1. Starter Standard Layered Structure
Spring Boot officially recommends a two‑segment split:
xxx-spring-boot-autoconfigure : the auto‑configuration module that contains all configuration classes, post‑processors, utility beans, conditional logic, and property binding.
xxx-spring-boot-starter : a pure dependency‑management module (no Java code) that pulls in the autoconfigure module and any third‑party dependencies.
Project Layout Example
custom-common-starter
├─ custom-common-autoconfigure // auto‑configuration core module
│ ├─ src/main/java/com/demo/common
│ │ ├─ config // auto‑configuration classes
│ │ ├─ handler // global exception & unified response
│ │ ├─ prop // configuration‑property binding
│ │ └─ util // common utility beans
│ └─ src/main/resources/META-INF
│ └─ spring.factories // auto‑configuration entry file
└─ custom-common-starter // pure dependency aggregation module
└─ pom.xml2. Build the Autoconfigure Module
2.1 Maven Basic Dependencies
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.15</version>
<relativePath/>
</parent>
<groupId>com.demo</groupId>
<artifactId>custom-common-autoconfigure</artifactId>
<version>1.0.0</version>
<dependencies>
<!-- Spring Boot core auto‑configuration -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Web environment for global exception & response wrapping -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<!-- Configuration properties processor -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>2.2 Custom Configuration‑Property Class
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "custom.common")
public class CommonProperties {
/** Enable unified response wrapping, default true */
private boolean enableResponseWrap = true;
/** Enable global exception handling, default true */
private boolean enableGlobalException = true;
// getters & setters
public boolean isEnableResponseWrap() { return enableResponseWrap; }
public void setEnableResponseWrap(boolean enableResponseWrap) { this.enableResponseWrap = enableResponseWrap; }
public boolean isEnableGlobalException() { return enableGlobalException; }
public void setEnableGlobalException(boolean enableGlobalException) { this.enableGlobalException = enableGlobalException; }
}2.3 Unified Result Entity
public class Result<T> {
private Integer code;
private String msg;
private T data;
public static <T> Result<T> success(T data) {
Result<T> res = new Result<>();
res.setCode(200);
res.setMsg("操作成功");
res.setData(data);
return res;
}
public static <T> Result<T> fail(Integer code, String msg) {
Result<T> res = new Result<>();
res.setCode(code);
res.setMsg(msg);
return res;
}
// getters & setters
}2.4 Global Response Advice
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@RestControllerAdvice
public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> {
private final CommonProperties properties;
public GlobalResponseAdvice(CommonProperties properties) { this.properties = properties; }
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// disabled via property -> no effect
return properties.isEnableResponseWrap();
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
// avoid double wrapping
if (body instanceof Result) {
return body;
}
return Result.success(body);
}
}2.5 Global Exception Advice
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionAdvice {
private final CommonProperties properties;
public GlobalExceptionAdvice(CommonProperties properties) { this.properties = properties; }
@ExceptionHandler(Exception.class)
public Result<Object> handleException(Exception e) {
if (!properties.isEnableGlobalException()) {
throw new RuntimeException(e);
}
return Result.fail(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务异常:" + e.getMessage());
}
}2.6 Core Auto‑Configuration Class with Conditional Annotations
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@Configuration
@EnableConfigurationProperties(CommonProperties.class)
@ConditionalOnClass(ResponseBodyAdvice.class) // only load in web projects
public class CommonAutoConfiguration {
private final CommonProperties commonProperties;
public CommonAutoConfiguration(CommonProperties commonProperties) { this.commonProperties = commonProperties; }
@Bean
@ConditionalOnProperty(prefix = "custom.common", name = "enable-response-wrap", havingValue = "true", matchIfMissing = true)
public GlobalResponseAdvice globalResponseAdvice() {
return new GlobalResponseAdvice(commonProperties);
}
@Bean
@ConditionalOnProperty(prefix = "custom.common", name = "enable-global-exception", havingValue = "true", matchIfMissing = true)
public GlobalExceptionAdvice globalExceptionAdvice() {
return new GlobalExceptionAdvice(commonProperties);
}
}2.7 Register Auto‑Configuration Entry
Create resources/META-INF/spring.factories with the fully‑qualified name of the configuration class:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.demo.common.config.CommonAutoConfiguration3. Build the Starter Aggregation Module
The module custom-common-starter only declares a dependency on the autoconfigure module, allowing business projects to import a single starter.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.15</version>
<relativePath/>
</parent>
<groupId>com.demo</groupId>
<artifactId>custom-common-starter</artifactId>
<version>1.0.0</version>
<name>自定义通用组件starter</name>
<dependencies>
<!-- Pull in the autoconfigure core module -->
<dependency>
<groupId>com.demo</groupId>
<artifactId>custom-common-autoconfigure</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</project>4. Use the Starter in a Business Project
4.1 Add Dependency
<dependency>
<groupId>com.demo</groupId>
<artifactId>custom-common-starter</artifactId>
<version>1.0.0</version>
</dependency>4.2 Configure Switches in application.yml
custom:
common:
# disable unified response wrapper, default true
enable-response-wrap: true
# disable global exception handling, default true
enable-global-exception: true4.3 Test Controller
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/info")
public String info() {
return "测试数据";
}
}When the endpoint is accessed, the response is automatically wrapped into the unified Result format; any thrown exception is caught by the global handler and returned as a standardized error structure without additional configuration in the business project.
5. Core Underlying Principles and Conditional Annotations
5.1 Auto‑Configuration Mechanism
During startup Spring Boot uses SpringFactoriesLoader to read all META-INF/spring.factories files on the classpath, finds entries under EnableAutoConfiguration, instantiates the listed configuration classes, and registers their beans automatically.
5.2 Common Conditional Annotations Required for Starters
@ConditionalOnClass: only load if the specified class is present, enabling separation of web and non‑web environments. @ConditionalOnMissingBean: create a bean only when the application does not already define one, allowing projects to override starter defaults. @ConditionalOnProperty: activate a component based on a property value defined in application.yml. @ConditionalOnWebApplication: load only in a web context.
5.3 Extension – Allow Business Projects to Override Built‑in Beans
By adding @ConditionalOnMissingBean to bean definitions, a business project can provide its own implementation of GlobalResponseAdvice or other components, which will replace the starter’s default.
6. Production‑Ready Guidelines
Strictly separate modules: the autoconfigure module holds all logic; the starter module only aggregates dependencies and must contain no Java code.
Expose every toggleable capability as a configuration property so projects can disable unwanted features.
Mark all built‑in beans with @ConditionalOnMissingBean to support custom overrides.
Declare web‑related dependencies in the autoconfigure module with provided scope to avoid version conflicts.
Include spring-boot-configuration-processor to enable IDE assistance for application.yml properties.
Use conditional annotations to differentiate web and non‑web environments, ensuring the starter does not cause errors in plain Java applications.
Custom starters are essential for engineering‑oriented, component‑based development. They unify common tools across teams, reduce repetitive code, and clearly separate ordinary business development from architectural concerns.
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.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
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.
