feat:框架完善,增加完整的支持

This commit is contained in:
wangyu 2021-09-28 21:24:23 +08:00
parent 9100c2214a
commit c6682526eb
9 changed files with 264 additions and 14 deletions

View File

@ -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 {};
}

View File

@ -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);
}

View File

@ -24,6 +24,6 @@ public class MoneyValidator implements ConstraintValidator<Money, Long> {
*/ */
@Override @Override
public boolean isValid(Long value, ConstraintValidatorContext context) { public boolean isValid(Long value, ConstraintValidatorContext context) {
return value >= 9; return null == value || value >= 9;
} }
} }

View File

@ -33,6 +33,9 @@ public class PhoneValidator implements ConstraintValidator<Phone, String> {
*/ */
@Override @Override
public boolean isValid(String value, ConstraintValidatorContext context) { public boolean isValid(String value, ConstraintValidatorContext context) {
if (null == value) {
return true;
}
if (type == PhoneType.ALL) { if (type == PhoneType.ALL) {
return isMobile(value) || isTel(value); return isMobile(value) || isTel(value);
} }

View File

@ -3,7 +3,10 @@ package com.flyfish.framework.configuration;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.flyfish.framework.configuration.resolver.PageQueryArgumentResolver; 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.UserArgumentResolver;
import com.flyfish.framework.configuration.resolver.ValidRequestBodyMethodArgumentResolver;
import com.flyfish.framework.context.SpringContext;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered; 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.EnableWebFlux;
import org.springframework.web.reactive.config.WebFluxConfigurer; import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; 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; import java.util.TimeZone;
/** /**
@ -33,9 +38,6 @@ public class WebfluxConfig implements WebFluxConfigurer {
@Value("${spring.jackson.date-format}") @Value("${spring.jackson.date-format}")
private String dateFormat = "yyyy-MM-dd HH:mm:ss"; private String dateFormat = "yyyy-MM-dd HH:mm:ss";
// @Resource
// private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
/** /**
* Configure resolvers for custom {@code @RequestMapping} method arguments. * Configure resolvers for custom {@code @RequestMapping} method arguments.
* *
@ -44,10 +46,11 @@ public class WebfluxConfig implements WebFluxConfigurer {
@Override @Override
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance(); ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance();
ServerCodecConfigurer serverCodecConfigurer = SpringContext.getBean(ServerCodecConfigurer.class);
configurer.addCustomResolver(new PageQueryArgumentResolver(registry)); configurer.addCustomResolver(new PageQueryArgumentResolver(registry));
configurer.addCustomResolver(new UserArgumentResolver(registry)); configurer.addCustomResolver(new UserArgumentResolver(registry));
// configurer.addCustomResolver(new RequestContextBodyArgumentResolver( configurer.addCustomResolver(new ValidRequestBodyMethodArgumentResolver(serverCodecConfigurer.getReaders(), registry));
// requestMappingHandlerAdapter.getMessageReaders(), registry)); configurer.addCustomResolver(new RequestContextBodyArgumentResolver(serverCodecConfigurer.getReaders(), registry));
} }
/** /**

View File

@ -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;
}

View File

@ -17,7 +17,7 @@ import reactor.core.publisher.Mono;
import java.util.List; import java.util.List;
/** /**
* 附带上下文的请求参数解析器 * 附带上下文的请求参数解析器
* *
* @author wangyu * @author wangyu
*/ */
@ -25,7 +25,6 @@ public class RequestContextBodyArgumentResolver extends AbstractMessageReaderArg
public RequestContextBodyArgumentResolver(List<HttpMessageReader<?>> readers, public RequestContextBodyArgumentResolver(List<HttpMessageReader<?>> readers,
ReactiveAdapterRegistry registry) { ReactiveAdapterRegistry registry) {
super(readers, registry); super(readers, registry);
} }
@ -38,7 +37,6 @@ public class RequestContextBodyArgumentResolver extends AbstractMessageReaderArg
@Override @Override
public Mono<Object> resolveArgument( public Mono<Object> resolveArgument(
MethodParameter param, BindingContext bindingContext, ServerWebExchange exchange) { MethodParameter param, BindingContext bindingContext, ServerWebExchange exchange) {
RequestBody ann = param.getParameterAnnotation(RequestBody.class); RequestBody ann = param.getParameterAnnotation(RequestBody.class);
Assert.state(ann != null, "No RequestBody annotation"); Assert.state(ann != null, "No RequestBody annotation");
return readBody(param, ann.required(), bindingContext, exchange) return readBody(param, ann.required(), bindingContext, exchange)

View File

@ -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);
}
}

View File

@ -5,6 +5,7 @@ import com.flyfish.framework.bean.Result;
import com.flyfish.framework.bean.SyncVo; import com.flyfish.framework.bean.SyncVo;
import com.flyfish.framework.configuration.annotations.CurrentUser; import com.flyfish.framework.configuration.annotations.CurrentUser;
import com.flyfish.framework.configuration.annotations.PagedQuery; import com.flyfish.framework.configuration.annotations.PagedQuery;
import com.flyfish.framework.configuration.annotations.ValidRequestBody;
import com.flyfish.framework.constant.ReactiveConstants; import com.flyfish.framework.constant.ReactiveConstants;
import com.flyfish.framework.context.UserContext; import com.flyfish.framework.context.UserContext;
import com.flyfish.framework.domain.base.Domain; 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 org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import javax.validation.Valid;
import java.util.List; import java.util.List;
/** /**
@ -51,7 +51,7 @@ public abstract class BaseController<T extends Domain, Q extends Qo<T>> implemen
@PostMapping("") @PostMapping("")
@Operation.Create @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); userContext.setUser(user);
return Result.accept(service.create(entity)); return Result.accept(service.create(entity));
} }
@ -63,7 +63,7 @@ public abstract class BaseController<T extends Domain, Q extends Qo<T>> implemen
@PutMapping("{id}") @PutMapping("{id}")
@Operation.Update @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); userContext.setUser(user);
return Result.accept(service.updateSelectiveById(entity)); return Result.accept(service.updateSelectiveById(entity));
} }
@ -110,7 +110,7 @@ public abstract class BaseController<T extends Domain, Q extends Qo<T>> implemen
* reactive接口 * reactive接口
*/ */
@PostMapping(value = "", headers = ReactiveConstants.USE_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); 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) @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) return reactiveService.updateById(entity)
.map(Result::accept) .map(Result::accept)
.defaultIfEmpty(Result.notFound()); .defaultIfEmpty(Result.notFound());