feat:框架完善,增加完整的支持
This commit is contained in:
parent
9100c2214a
commit
c6682526eb
@ -0,0 +1,20 @@
|
||||
package com.flyfish.framework.validation.spi;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.TYPE;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
/**
|
||||
* 指定动态分组
|
||||
* @author wangyu
|
||||
*/
|
||||
@Target({TYPE})
|
||||
@Retention(RUNTIME)
|
||||
@Documented
|
||||
public @interface ConditionalGroup {
|
||||
|
||||
Class<? extends ConditionalValidGroupProvider<?>>[] value() default {};
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package com.flyfish.framework.validation.spi;
|
||||
|
||||
import javax.validation.groups.Default;
|
||||
|
||||
/**
|
||||
* 条件化的验证分组
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
public interface ConditionalValidGroupProvider<T> {
|
||||
|
||||
/**
|
||||
* 根据数据返回不同的验证分组
|
||||
*
|
||||
* @param data 数据
|
||||
* @return 结果
|
||||
*/
|
||||
Class<?>[] provide(T data);
|
||||
}
|
@ -24,6 +24,6 @@ public class MoneyValidator implements ConstraintValidator<Money, Long> {
|
||||
*/
|
||||
@Override
|
||||
public boolean isValid(Long value, ConstraintValidatorContext context) {
|
||||
return value >= 9;
|
||||
return null == value || value >= 9;
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,9 @@ public class PhoneValidator implements ConstraintValidator<Phone, String> {
|
||||
*/
|
||||
@Override
|
||||
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||
if (null == value) {
|
||||
return true;
|
||||
}
|
||||
if (type == PhoneType.ALL) {
|
||||
return isMobile(value) || isTel(value);
|
||||
}
|
||||
|
@ -3,7 +3,10 @@ package com.flyfish.framework.configuration;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.flyfish.framework.configuration.resolver.PageQueryArgumentResolver;
|
||||
import com.flyfish.framework.configuration.resolver.RequestContextBodyArgumentResolver;
|
||||
import com.flyfish.framework.configuration.resolver.UserArgumentResolver;
|
||||
import com.flyfish.framework.configuration.resolver.ValidRequestBodyMethodArgumentResolver;
|
||||
import com.flyfish.framework.context.SpringContext;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.Ordered;
|
||||
@ -17,7 +20,9 @@ import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||
import org.springframework.web.reactive.config.EnableWebFlux;
|
||||
import org.springframework.web.reactive.config.WebFluxConfigurer;
|
||||
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
|
||||
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
@ -33,9 +38,6 @@ public class WebfluxConfig implements WebFluxConfigurer {
|
||||
@Value("${spring.jackson.date-format}")
|
||||
private String dateFormat = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
// @Resource
|
||||
// private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
|
||||
|
||||
/**
|
||||
* Configure resolvers for custom {@code @RequestMapping} method arguments.
|
||||
*
|
||||
@ -44,10 +46,11 @@ public class WebfluxConfig implements WebFluxConfigurer {
|
||||
@Override
|
||||
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
|
||||
ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance();
|
||||
ServerCodecConfigurer serverCodecConfigurer = SpringContext.getBean(ServerCodecConfigurer.class);
|
||||
configurer.addCustomResolver(new PageQueryArgumentResolver(registry));
|
||||
configurer.addCustomResolver(new UserArgumentResolver(registry));
|
||||
// configurer.addCustomResolver(new RequestContextBodyArgumentResolver(
|
||||
// requestMappingHandlerAdapter.getMessageReaders(), registry));
|
||||
configurer.addCustomResolver(new ValidRequestBodyMethodArgumentResolver(serverCodecConfigurer.getReaders(), registry));
|
||||
configurer.addCustomResolver(new RequestContextBodyArgumentResolver(serverCodecConfigurer.getReaders(), registry));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,16 @@
|
||||
package com.flyfish.framework.configuration.annotations;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 附带校验的请求体
|
||||
* 天然支持条件校验
|
||||
* @author wangyu
|
||||
*/
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface ValidRequestBody {
|
||||
|
||||
boolean required() default true;
|
||||
}
|
@ -17,7 +17,7 @@ import reactor.core.publisher.Mono;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 附带上下文你的请求参数解析器
|
||||
* 附带上下文里的请求参数解析器
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@ -25,7 +25,6 @@ public class RequestContextBodyArgumentResolver extends AbstractMessageReaderArg
|
||||
|
||||
public RequestContextBodyArgumentResolver(List<HttpMessageReader<?>> readers,
|
||||
ReactiveAdapterRegistry registry) {
|
||||
|
||||
super(readers, registry);
|
||||
}
|
||||
|
||||
@ -38,7 +37,6 @@ public class RequestContextBodyArgumentResolver extends AbstractMessageReaderArg
|
||||
@Override
|
||||
public Mono<Object> resolveArgument(
|
||||
MethodParameter param, BindingContext bindingContext, ServerWebExchange exchange) {
|
||||
|
||||
RequestBody ann = param.getParameterAnnotation(RequestBody.class);
|
||||
Assert.state(ann != null, "No RequestBody annotation");
|
||||
return readBody(param, ann.required(), bindingContext, exchange)
|
||||
|
@ -0,0 +1,191 @@
|
||||
package com.flyfish.framework.configuration.resolver;
|
||||
|
||||
import com.flyfish.framework.configuration.annotations.ValidRequestBody;
|
||||
import com.flyfish.framework.validation.spi.ConditionalGroup;
|
||||
import com.flyfish.framework.validation.spi.ConditionalValidGroupProvider;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.springframework.core.*;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.codec.DecodingException;
|
||||
import org.springframework.core.codec.Hints;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.data.util.CastUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.bind.support.WebExchangeBindException;
|
||||
import org.springframework.web.bind.support.WebExchangeDataBinder;
|
||||
import org.springframework.web.reactive.BindingContext;
|
||||
import org.springframework.web.reactive.result.method.annotation.AbstractMessageReaderArgumentResolver;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import javax.validation.groups.Default;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 私有校验逻辑支持的body
|
||||
*
|
||||
* @author wangyu
|
||||
* 支持条件校验,更高级的骚操作,大量重写
|
||||
*/
|
||||
public class ValidRequestBodyMethodArgumentResolver extends AbstractMessageReaderArgumentResolver {
|
||||
|
||||
private static final Set<HttpMethod> SUPPORTED_METHODS =
|
||||
EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH);
|
||||
|
||||
private final List<MediaType> supportedMediaTypes;
|
||||
|
||||
public ValidRequestBodyMethodArgumentResolver(List<HttpMessageReader<?>> readers, ReactiveAdapterRegistry registry) {
|
||||
super(readers, registry);
|
||||
supportedMediaTypes = readers.stream()
|
||||
.flatMap(converter -> converter.getReadableMediaTypes().stream())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return parameter.hasParameterAnnotation(ValidRequestBody.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Object> resolveArgument(
|
||||
MethodParameter param, BindingContext bindingContext, ServerWebExchange exchange) {
|
||||
|
||||
ValidRequestBody ann = param.getParameterAnnotation(ValidRequestBody.class);
|
||||
Assert.state(ann != null, "No RequestBody annotation");
|
||||
return readBody(param, ann.required(), bindingContext, exchange);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<Object> readBody(MethodParameter bodyParam, @Nullable MethodParameter actualParam,
|
||||
boolean isBodyRequired, BindingContext bindingContext, ServerWebExchange exchange) {
|
||||
|
||||
ResolvableType bodyType = ResolvableType.forMethodParameter(bodyParam);
|
||||
ResolvableType actualType = (actualParam != null ? ResolvableType.forMethodParameter(actualParam) : bodyType);
|
||||
Class<?> resolvedType = bodyType.resolve();
|
||||
ReactiveAdapter adapter = (resolvedType != null ? getAdapterRegistry().getAdapter(resolvedType) : null);
|
||||
ResolvableType elementType = (adapter != null ? bodyType.getGeneric() : bodyType);
|
||||
isBodyRequired = isBodyRequired || (adapter != null && !adapter.supportsEmpty());
|
||||
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
|
||||
MediaType contentType = request.getHeaders().getContentType();
|
||||
MediaType mediaType = (contentType != null ? contentType : MediaType.APPLICATION_OCTET_STREAM);
|
||||
|
||||
if (mediaType.isCompatibleWith(MediaType.APPLICATION_FORM_URLENCODED)) {
|
||||
return Mono.error(new IllegalStateException(
|
||||
"In a WebFlux application, form data is accessed via ServerWebExchange.getFormData()."));
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(exchange.getLogPrefix() + (contentType != null ?
|
||||
"Content-Type:" + contentType :
|
||||
"No Content-Type, using " + MediaType.APPLICATION_OCTET_STREAM));
|
||||
}
|
||||
|
||||
for (HttpMessageReader<?> reader : getMessageReaders()) {
|
||||
if (reader.canRead(elementType, mediaType)) {
|
||||
Map<String, Object> readHints = Hints.from(Hints.LOG_PREFIX_HINT, exchange.getLogPrefix());
|
||||
if (adapter != null && adapter.isMultiValue()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(exchange.getLogPrefix() + "0..N [" + elementType + "]");
|
||||
}
|
||||
Flux<?> flux = reader.read(actualType, elementType, request, response, readHints);
|
||||
flux = flux.onErrorResume(ex -> Flux.error(handleReadError(bodyParam, ex)));
|
||||
if (isBodyRequired) {
|
||||
flux = flux.switchIfEmpty(Flux.error(() -> handleMissingBody(bodyParam)));
|
||||
}
|
||||
flux = flux.doOnNext(target -> validate(target, bodyParam, bindingContext, exchange));
|
||||
return Mono.just(adapter.fromPublisher(flux));
|
||||
} else {
|
||||
// Single-value (with or without reactive type wrapper)
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(exchange.getLogPrefix() + "0..1 [" + elementType + "]");
|
||||
}
|
||||
Mono<?> mono = reader.readMono(actualType, elementType, request, response, readHints);
|
||||
mono = mono.onErrorResume(ex -> Mono.error(handleReadError(bodyParam, ex)));
|
||||
if (isBodyRequired) {
|
||||
mono = mono.switchIfEmpty(Mono.error(() -> handleMissingBody(bodyParam)));
|
||||
}
|
||||
mono = mono.doOnNext(target -> validate(target, bodyParam, bindingContext, exchange));
|
||||
return (adapter != null ? Mono.just(adapter.fromPublisher(mono)) : Mono.from(mono));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No compatible reader but body may be empty..
|
||||
|
||||
HttpMethod method = request.getMethod();
|
||||
if (contentType == null && method != null && SUPPORTED_METHODS.contains(method)) {
|
||||
Flux<DataBuffer> body = request.getBody().doOnNext(buffer -> {
|
||||
DataBufferUtils.release(buffer);
|
||||
// Body not empty, back to 415..
|
||||
throw new UnsupportedMediaTypeStatusException(mediaType, this.supportedMediaTypes, elementType);
|
||||
});
|
||||
if (isBodyRequired) {
|
||||
body = body.switchIfEmpty(Mono.error(() -> handleMissingBody(bodyParam)));
|
||||
}
|
||||
return (adapter != null ? Mono.just(adapter.fromPublisher(body)) : Mono.from(body));
|
||||
}
|
||||
|
||||
return Mono.error(new UnsupportedMediaTypeStatusException(mediaType, this.supportedMediaTypes, elementType));
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心,从对象抽取出验证的group
|
||||
*
|
||||
* @param target 目标对象
|
||||
* @return 结果
|
||||
*/
|
||||
@NonNull
|
||||
private Object[] extractValidationHints(Object target) {
|
||||
ConditionalGroup group = AnnotationUtils.findAnnotation(target.getClass(), ConditionalGroup.class);
|
||||
if (null != group && ArrayUtils.isNotEmpty(group.value())) {
|
||||
List<Class<?>> groups = new ArrayList<>();
|
||||
for (Class<? extends ConditionalValidGroupProvider<?>> clazz : group.value()) {
|
||||
try {
|
||||
groups.addAll(Arrays.asList(clazz.newInstance().provide(CastUtils.cast(target))));
|
||||
} catch (InstantiationException | IllegalAccessException ignored) {
|
||||
}
|
||||
}
|
||||
if (CollectionUtils.isNotEmpty(groups)) {
|
||||
return groups.toArray();
|
||||
}
|
||||
}
|
||||
return new Object[]{};
|
||||
}
|
||||
|
||||
private void validate(Object target, MethodParameter param,
|
||||
BindingContext binding, ServerWebExchange exchange) {
|
||||
Object[] validationHints = extractValidationHints(target);
|
||||
String name = Conventions.getVariableNameForParameter(param);
|
||||
WebExchangeDataBinder binder = binding.createDataBinder(exchange, target, name);
|
||||
binder.validate(validationHints);
|
||||
if (binder.getBindingResult().hasErrors()) {
|
||||
throw new WebExchangeBindException(param, binder.getBindingResult());
|
||||
}
|
||||
}
|
||||
|
||||
private Throwable handleReadError(MethodParameter parameter, Throwable ex) {
|
||||
return (ex instanceof DecodingException ?
|
||||
new ServerWebInputException("Failed to read HTTP message", parameter, ex) : ex);
|
||||
}
|
||||
|
||||
private ServerWebInputException handleMissingBody(MethodParameter parameter) {
|
||||
String paramInfo = parameter.getExecutable().toGenericString();
|
||||
return new ServerWebInputException("Request body is missing: " + paramInfo, parameter);
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import com.flyfish.framework.bean.Result;
|
||||
import com.flyfish.framework.bean.SyncVo;
|
||||
import com.flyfish.framework.configuration.annotations.CurrentUser;
|
||||
import com.flyfish.framework.configuration.annotations.PagedQuery;
|
||||
import com.flyfish.framework.configuration.annotations.ValidRequestBody;
|
||||
import com.flyfish.framework.constant.ReactiveConstants;
|
||||
import com.flyfish.framework.context.UserContext;
|
||||
import com.flyfish.framework.domain.base.Domain;
|
||||
@ -16,7 +17,6 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@ -51,7 +51,7 @@ public abstract class BaseController<T extends Domain, Q extends Qo<T>> implemen
|
||||
|
||||
@PostMapping("")
|
||||
@Operation.Create
|
||||
public Result<T> create(@RequestBody @Valid T entity, @CurrentUser User user) {
|
||||
public Result<T> create(@ValidRequestBody T entity, @CurrentUser User user) {
|
||||
userContext.setUser(user);
|
||||
return Result.accept(service.create(entity));
|
||||
}
|
||||
@ -63,7 +63,7 @@ public abstract class BaseController<T extends Domain, Q extends Qo<T>> implemen
|
||||
|
||||
@PutMapping("{id}")
|
||||
@Operation.Update
|
||||
public Result<T> update(@RequestBody @Valid T entity, @CurrentUser User user) {
|
||||
public Result<T> update(@ValidRequestBody T entity, @CurrentUser User user) {
|
||||
userContext.setUser(user);
|
||||
return Result.accept(service.updateSelectiveById(entity));
|
||||
}
|
||||
@ -110,7 +110,7 @@ public abstract class BaseController<T extends Domain, Q extends Qo<T>> implemen
|
||||
* reactive接口
|
||||
*/
|
||||
@PostMapping(value = "", headers = ReactiveConstants.USE_REACTIVE)
|
||||
public Mono<Result<T>> reactiveCreate(@RequestBody @Valid T entity) {
|
||||
public Mono<Result<T>> reactiveCreate(@ValidRequestBody T entity) {
|
||||
return reactiveService.create(entity).map(Result::accept);
|
||||
}
|
||||
|
||||
@ -122,7 +122,7 @@ public abstract class BaseController<T extends Domain, Q extends Qo<T>> implemen
|
||||
}
|
||||
|
||||
@PutMapping(value = "{id}", headers = ReactiveConstants.USE_REACTIVE)
|
||||
public Mono<Result<T>> reactiveUpdate(@RequestBody @Valid T entity) {
|
||||
public Mono<Result<T>> reactiveUpdate(@ValidRequestBody T entity) {
|
||||
return reactiveService.updateById(entity)
|
||||
.map(Result::accept)
|
||||
.defaultIfEmpty(Result.notFound());
|
||||
|
Loading…
Reference in New Issue
Block a user