Spring Boot替代Swagger UI:自动生成DTO/实体结构与校验信息
Solution to Auto-Generate DTO Metadata for Custom Actuator Endpoint
To address your issue of automatically displaying DTO structures and validation annotations for endpoints with identical URIs but different parameters, follow this practical implementation:
1. Core Approach
We’ll leverage Spring’s internal handler mapping to extract all endpoint metadata, use reflection to scan DTO classes for field and validation details, and expose this combined data via your custom actuator endpoint.
2. Implement Metadata Collection Service
Create a service to fetch endpoint details and inspect DTO classes:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import javax.validation.constraints.*; import java.lang.reflect.Field; import java.util.*; import java.util.stream.Collectors; @Service public class EndpointMetadataService { private final RequestMappingHandlerMapping handlerMapping; @Autowired public EndpointMetadataService(RequestMappingHandlerMapping handlerMapping) { this.handlerMapping = handlerMapping; } // Define your DTO/model packages here private static final List<String> SCAN_PACKAGES = Arrays.asList("com.yourproject.dto", "com.yourproject.model"); public List<EndpointMetadata> getAllEndpointMetadata() { List<EndpointMetadata> metadataList = new ArrayList<>(); // Iterate all registered handler methods handlerMapping.getHandlerMethods().forEach((requestMappingInfo, handlerMethod) -> { // Extract endpoint path String path = requestMappingInfo.getPatternsCondition().getPatterns().iterator().next(); // Extract HTTP method String method = requestMappingInfo.getMethodsCondition().getMethods().iterator().next().name(); // Extract differentiating parameters (e.g., type=customer) Set<String> params = requestMappingInfo.getParamsCondition().getExpressions(); // Find request body type for the endpoint Optional<Class<?>> requestBodyType = handlerMethod.getMethodParameters().stream() .filter(param -> param.hasParameterAnnotation(RequestBody.class)) .map(param -> param.getParameterType()) .findFirst(); if (requestBodyType.isPresent()) { Class<?> dtoClass = requestBodyType.get(); List<FieldMetadata> fieldMetadata = getDtoFieldDetails(dtoClass); metadataList.add(new EndpointMetadata(path, method, params, dtoClass.getSimpleName(), fieldMetadata)); } else { // Handle endpoints without request bodies metadataList.add(new EndpointMetadata(path, method, params, null, Collections.emptyList())); } }); return metadataList; } // Extract field names, types, and validation annotations from DTO class private List<FieldMetadata> getDtoFieldDetails(Class<?> clazz) { List<FieldMetadata> fieldMetadataList = new ArrayList<>(); List<Field> fields = getAllFields(clazz); // Include inherited fields for (Field field : fields) { field.setAccessible(true); FieldMetadata fieldMetadata = new FieldMetadata(); fieldMetadata.setFieldName(field.getName()); fieldMetadata.setFieldType(field.getType().getSimpleName()); List<String> validations = new ArrayList<>(); if (field.isAnnotationPresent(NotBlank.class)) validations.add("@NotBlank"); if (field.isAnnotationPresent(NotNull.class)) validations.add("@NotNull"); if (field.isAnnotationPresent(Email.class)) validations.add("@Email"); if (field.isAnnotationPresent(Min.class)) { Min min = field.getAnnotation(Min.class); validations.add("@Min(value = " + min.value() + ")"); } if (field.isAnnotationPresent(Max.class)) { Max max = field.getAnnotation(Max.class); validations.add("@Max(value = " + max.value() + ")"); } // Add other validation annotations as needed (e.g., @Pattern, @Size) fieldMetadata.setValidationAnnotations(validations); fieldMetadataList.add(fieldMetadata); } return fieldMetadataList; } // Recursively get all fields including superclass fields private List<Field> getAllFields(Class<?> clazz) { List<Field> fields = new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())); Class<?> superClass = clazz.getSuperclass(); if (superClass != null && !superClass.equals(Object.class)) { fields.addAll(getAllFields(superClass)); } return fields; } // Data classes to hold metadata public static class EndpointMetadata { private String path; private String method; private Set<String> params; private String requestBodyType; private List<FieldMetadata> fields; public EndpointMetadata(String path, String method, Set<String> params, String requestBodyType, List<FieldMetadata> fields) { this.path = path; this.method = method; this.params = params; this.requestBodyType = requestBodyType; this.fields = fields; } // Getters for serialization public String getPath() { return path; } public String getMethod() { return method; } public Set<String> getParams() { return params; } public String getRequestBodyType() { return requestBodyType; } public List<FieldMetadata> getFields() { return fields; } } public static class FieldMetadata { private String fieldName; private String fieldType; private List<String> validationAnnotations; // Getters and setters public String getFieldName() { return fieldName; } public void setFieldName(String fieldName) { this.fieldName = fieldName; } public String getFieldType() { return fieldType; } public void setFieldType(String fieldType) { this.fieldType = fieldType; } public List<String> getValidationAnnotations() { return validationAnnotations; } public void setValidationAnnotations(List<String> validationAnnotations) { this.validationAnnotations = validationAnnotations; } } }
3. Integrate with Custom Actuator Endpoint
Update your existing custom endpoint to use the metadata service:
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.stereotype.Component; import java.util.List; @Component @Endpoint(id = "customEndpoints") public class CustomEndpoint { private final EndpointMetadataService metadataService; public CustomEndpoint(EndpointMetadataService metadataService) { this.metadataService = metadataService; } @ReadOperation public List<EndpointMetadataService.EndpointMetadata> getEndpointDetails() { return metadataService.getAllEndpointMetadata(); } }
4. Key Features & Benefits
- Captures All Endpoints: Uses Spring’s
RequestMappingHandlerMappingto fetch every registered endpoint, including those differentiated byparams(like your/api/userswithtype=customerandtype=owner). - Auto-Generates DTO Details: Reflection scans DTO fields to extract names, types, and validation annotations (e.g.,
@NotBlank,@Email) without manual effort. - Low Maintenance: Automatically updates metadata when you add/modify endpoints or DTOs, eliminating inconsistencies.
5. Optional Enhancements
- Nested DTO Support: Extend
getDtoFieldDetailsto recursively inspect nested objects and include their structure. - Class Filtering: Add filters to only scan classes marked with a custom annotation (e.g.,
@ApiDto) if you don’t want to include all classes in the target packages. - Caching: Cache the metadata after initial scan to improve startup performance for large projects.
内容的提问来源于stack exchange,提问作者Nick M.
相关产品推荐
相关产品推荐

