如何在 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 错误,但您可以轻松地将其映射到您想要的任何内容。
我正在使用 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 错误,但您可以轻松地将其映射到您想要的任何内容。