如何在 Java 中创建注释并访问它创建的变量?

How do I create an annotation in Java and access variables it creates?

我正在使用 Spring Boot 创建一个非常标准的 REST 服务。这部分意味着很多方法将 return 页结果,因此输入到方法中将需要页码、页面大小等 - 例如:

    @GetMapping(
            value = "",
            produces = MediaType.APPLICATION_JSON_VALUE
    )
    @ResponseBody
    ResponseEntity readAll(
            @RequestParam("pn") Integer pageNumber,
            @RequestParam("ps") Integer pageSize,
            @RequestParam("sc") String sortColumn,
            @RequestParam("so") String sortOrder
    ) {

然后在方法本身中我们将验证参数(例如,确保页面大小 < 100、排序列是允许的值等)。然后将这些值转换成可用的东西——例如从列和顺序创建一个 Sort 对象,然后从页码、页面大小和排序对象创建一个 Pageable 对象。最终将其传递到 JPA 存储库以 return 一页结果。

由于这个模式会一遍又一遍地重复,我只想做一个封装所有这些的注释,但我不确定如何将 4 个 RequestParam 变量转换为单个 Pageable 对象,或如何访问在方法主体的注释中创建的对象。

我尝试了一些基本的注释工作,例如

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ServicePageSizeValidator.class)
public @interface ServicePageSize {
    Integer DEFAULT_MAX = 100;
    String max() default "100";
}

class ServicePageSizeValidator
        implements ConstraintValidator<ServicePageSize, Integer> {
    private static final Logger LOGGER =
            LoggerFactory.getLogger(ServicePageSizeValidator.class);

    private Integer max = ServicePageSize.DEFAULT_MAX;

    @Override
    public void initialize(final ServicePageSize annotation) {
        try {
            max = Integer.valueOf(annotation.max());
        } catch (Exception e) {
            LOGGER.warn(
                    "Exception caught trying to parse page size " +
                            "constraint {}.", annotation.max(), e
            );
            max = ServicePageSize.DEFAULT_MAX;
        }
    }

    @Override
    public boolean isValid(final Integer pageSize,
                           final ConstraintValidatorContext context) {
        try {
            if (pageSize == null) {
                throw new InvalidPageSizeException("Null page size.");
            }
            if (pageSize > max) {
                throw new InvalidPageSizeException(
                        "Page size " + pageSize + "larger than maximum " +
                                "allowed (" + max + ")."
                );
            }
        } catch (Exception e) {
            LOGGER.warn("Invalid page size.", e);
            return false;
        }
        return true;
    }
}

这通常似乎可行,但它仅在单个参数上运行,如果参数错误,则整个注释都会失败 - 假设它通过了,我无法在方法中访问有关验证的信息.

作为一个问题保持开放可能仍然是合理的,因为对于其他需要的注释可能存在类似的情况。但是对于分页至少 Spring Boot 显然已经涵盖了我。

(外部 link):https://reflectoring.io/spring-boot-paging/

本质上:

ResponseEntity readAll(Pageable pageable)

将起作用,并且可以通过多种方式配置 URL 参数名称,例如在 application.properties:

spring.data.web.pageable.size-parameter=size
spring.data.web.pageable.page-parameter=page
spring.data.web.pageable.default-page-size=20
spring.data.web.pageable.max-page-size=2000

或通过注释:

ResponseEntity readAll(
    @PageableDefault(page = 0, size = 20) Pageable pageable
)

我将保持开放状态,这不是公认的答案,因为我觉得关于如何正确执行此操作的更普遍的问题仍然有效。但看起来答案可能是创建一个 class 将请求参数自映射到自身,然后在传入的单个对象上使用注释来配置它。

正如@DrTeeth 所提到的,Spring 已经提供了一个很好的分页功能,所以我会坚持使用它,因为它为您提供了也可以与存储库一起使用的选项。 Pageable 绝对是非常有用的。

一般来说,您可以使用 JEE Validation API 执行此类验证。此外,您可以使用 Spring 解析器以一种非常简单的方式在每个需要它们的控制器中提供参数。让我举一个例子。首先,让我们创建一个 class 来映射您的参数:

public class Pagination {
    @Max(100)
    int pageNumber;

    int pageSize;
    String sortColumn;
    String sortOrder;
}

如您所见,我已经在使用验证 API。您可以通过在您的 pom 中添加此依赖项来将此功能添加到您的 Spring 项目中:

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

出于示例的目的,我只在页码上添加了验证。 您现在可以创建一个界面。稍后我会解释它的用途。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface PaginationConfig {
}

这将告诉 Spring 它可以预期将 PaginationConfig 用作方法参数的装饰器。 现在是我们解析器的时候了:

public class PaginationResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        return methodParameter.getParameterAnnotation(PaginationConfig.class) != null;
    }

    @Override
    public Object resolveArgument(
            MethodParameter methodParameter,
            ModelAndViewContainer modelAndViewContainer,
            NativeWebRequest nativeWebRequest,
            WebDataBinderFactory webDataBinderFactory) throws BadRequestException {
        Pagination pagination = new Pagination();
        HttpServletRequest request
                = (HttpServletRequest) nativeWebRequest.getNativeRequest();

        try {
            pagination.setPageNumber(Integer.parseInt(request.getParameter("pn")));
            pagination.setPageSize(Integer.parseInt(request.getParameter("ps")));
        } catch (NumberFormatException e) {
            throw new BadRequestException();
        }
        pagination.setSortColumn(request.getParameter("sc"));
        pagination.setSortOrder(request.getParameter("so"));

        return pagination;
    }
}

它绑定到我们刚刚创建的接口,它从请求中读取参数值,创建一个Pagination对象。请注意,验证尚未发生。我们只是告诉 Spring 如何读取这些值,并捕获 NumberFormatException,因此我们可以在自定义异常中转换它(我在这个例子中调用了 BadRequestException 并且我将映射到 BAD_REQUEST HTTP 响应代码)。

我们快完成了。我们现在必须添加一些配置:

@Configuration
public class MyConfig implements WebMvcConfigurer {
    @Override
    public void addArgumentResolvers(
            List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(new PaginationResolver());
    }

    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}

通过这种方式,我们告诉 Spring 它必须使用自定义解析器并且必须验证方法的参数。 现在是我们进入控制器的时候了。我给你举个例子:

@RestController
@Validated
public class MyController {

    @GetMapping
    public String validateAndPaginate(@Valid @PaginationConfig Pagination pagination) {
        return "hello world " + pagination.getPageSize();
    }
}

注意事项:

  • 我们需要在class级别使用@Validated;
  • 我们需要在参数级别使用@Valid;
  • 我们正在使用我们创建的接口 @PaginationConfig 装饰参数。 Spring 知道如何找到需要在该对象中绑定的值,因为我们已经告诉它使用我们的解析器;
  • 我们正在使用验证 API,因此所有内容都将根据我们放入分页中的注释进行验证 class;
  • 如果验证失败,Spring 会给我们一个 500 错误,但您可以轻松地将其映射到您想要的任何内容。