You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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 RequestMappingHandlerMapping to fetch every registered endpoint, including those differentiated by params (like your /api/users with type=customer and type=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 getDtoFieldDetails to 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.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 04:23:14