自定义springboot参数解析器

本文介绍了如何通过自定义注解和参数解析器解决Spring Boot中@RequestParam注解带来的问题,如非空校验响应码和接收POST body参数。作者创建了名为@Par的自定义注解,并实现了一个名为MyArgumentResolver的参数解析器,同时使用线程变量RequestInfo来存储请求参数,以在多个方法间共享。此外,文中还展示了如何在切面中清除线程变量,以及配置参数解析器。

自带的@RequestParam解析器的问题

一、@RequestParam参数默认有非空校验,非空校验我们当然要做,只是springboot的非空校验返回的是request的错误码,我需要此时请求响应码是200,而且需要根据不同接口返回不一样的提示。
二、@RequestParam参数不能接收post的body中的参数。
经过研究,可以通过自定义参数解析器解决这两个问题。

自定义注解Par

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Par {
    String value();
    String defaultValue() default ValueConstants.DEFAULT_NONE;
}

自定义参数解析器

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.annotation.ValueConstants;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver;
import javax.servlet.http.HttpServletRequest;
/**
 * @author chenzy
 * @since 2020-12-03
 */
public class MyArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        return methodParameter.hasParameterAnnotation(Par.class);
    }

    @Override
    protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
        return new RequestParamNamedValueInfo(parameter.getParameterAnnotation(Par.class));
    }

    @Override
    protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) {
        if (RequestInfo.instance().unInit()){
            RequestInfo.instance().setPar(RequestUtil.getMap((HttpServletRequest) request.getNativeRequest()));
        }
        return RequestInfo.instance().get(name.strip(),parameter.getParameterType());
    }
    private static class RequestParamNamedValueInfo extends NamedValueInfo {
        public RequestParamNamedValueInfo() {
            super("", false, ValueConstants.DEFAULT_NONE);
        }
        public RequestParamNamedValueInfo(Par annotation) {
            super(annotation.value(), false, annotation.defaultValue());
        }
    }
}

MyArgumentResolver .resolveName中request.getParameter(name); 只能获取url后的参数,body中的数据无法获取。
所以在RequestUtil.getMap(HttpServletRequest req)中获取所有数据。代码如下,

/**
     * request 请求参数
     */
    public static StringMap<String> getMap(HttpServletRequest req) {
        StringMap<String> par = new StringMap<>();
        /*获取请求头参数*/
        req.getParameterMap().forEach((key,values)-> par.put(key.strip(), StringUtil.join(values).strip()));
        /*获取body参数*/
        par.putAll(getPost(req));
        return par;
    }
     private static StringMap<String> getPost(HttpServletRequest req) {
        if (req.getContentLength()< 1) {
            return StringMap.empty();
        }
        try(var reader=req.getInputStream()){
            /*使用@RequestBody注解后,不能再读取请求体数据*/
            if (reader.isFinished()){
                return StringMap.empty();
            }
            var json = new String(reader.readAllBytes(), Const.charset);
            if (json.length() < 1) {
                return StringMap.empty();
            }
            return StringMap.translate(JsonUtil.str2Model(json, Map.class));
        } catch (IOException e) {
            e.printStackTrace();
            return StringMap.empty();
        }
    }

StringMap是我自定义的map类,是HashMap<String,Object>的子类,可用HashMap<String,String>替代StringMap<String>
JsonUtil是自己对json操作的工具类,在这不赘述
RequestInfo是单例对象,核心代码在下面,懂得都懂。

public class RequestInfo {
    private RequestInfo() {
    }
    private ThreadLocalMapPar par = new ThreadLocalMapPar();
        /*判断是否已经把请求信息存入*/
    public boolean unInit() {
        return !get("alreadyInit", Boolean.class, false);
    }
    public <T> T get(String name, Class<T> tClass, T defaultValue) {
      return par.get(name, tClass, defaultValue);
    }
    public <T> T get(String name, Class<T> tClass) {
        return par.get(name, tClass);
    }
    public void setPar(StringMap<String> par) {
      this.par.putALL(par);
      this.par.put("alreadyInit", true);
    }
	public void clear() {
        par.clear();
    }
}
/**
 * @author chenzy
 * @since 2020-12-03
 * 线程变量池
 */
public class ThreadLocalMapPar extends InheritableThreadLocal<StringMap<?>> {
    @Override
    protected StringMap<?> childValue(StringMap<?> parentValue) {
        if (parentValue == null) {
            return null;
        }
        return new StringMap<>(parentValue);
    }
    public StringMap<?> getCopyMap() {
        return (StringMap<?>) getMap().clone();
    }
    public <T> ThreadLocalMapPar put(String key, T val) {
        if (StringUtil.isBlankOr(key, val)) {
            return this;
        }
        StringMap map = getMap();
        map.add(key, val);
        return this;
    }
    public <T> T get(String key, Class<T> tClass,T defaultValue) {
        if (StringUtil.isBlank(key)) {
            return null;
        }
        StringMap map = getMap();
        Object value = map.get(key);
        if (value == null) {
            return defaultValue;
        }
        if (ClassUtil.isType(value, tClass)) {
            return tClass.cast(value);
        }
        return JsonUtil.model2Model(value, tClass);
    }
    public <T> T get(String key, Class<T> tClass) {
        return get(key,tClass,null);
    }
    public String get(String key) {
        if (StringUtil.isBlank(key)) {
            return "";
        }
        StringMap map = getMap();
        Object value = map.get(key);
        return value == null ? "" : JsonUtil.model2Str(value);
    }
    public ThreadLocalMapPar putALL(StringMap<?> par) {
        if (par==null) {
            return this;
        }
        StringMap map = getMap();
        map.putAll(par);
        return this;
    }
    public boolean containsKey(String key) {
        return getMap().containsKey(key);
    }
    private StringMap<?> getMap(){
        StringMap map = this.get();
        if (map == null) {
            map = new StringMap();
            this.set(map);
        }
        return map;
    }
    public void clear(){
        getMap().clear();
    }
}

切面 清除线程变量

需要注意的是,在切面中需要clear掉线程变量。以下是切面代码

public static AspectJExpressionPointcutAdvisor getAdvisor(String expression){
   AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
    advisor.setExpression(expression);
    advisor.setOrder(1);
    advisor.setAdvice((MethodInterceptor) invocation -> {
        try {
            /*参数存入RequestInfo*/
            HttpServletRequest request = (HttpServletRequest) RequestContextHolder.getRequestAttributes().resolveReference(RequestAttributes.REFERENCE_REQUEST);
            if (RequestInfo.instance().unInit()){
                RequestInfo.instance().setPar(RequestUtil.getMap(request));
            }
            //执行业务方法
            return invocation.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }finally {
            /*线程在线程池中复用,每个请求结束后需要把线程变量清掉。*/
            RequestInfo.instance().clear();
        }
    });
    return advisor;
}
 @Bean
 public AspectJExpressionPointcutAdvisor dataAspect(@Value("${spring.application.name}") String rootPath) {
     return AspectUtil.getAdvisor("execution(public * com.zhilei."+rootPath.replace("/","")+".*.control..*.*(..))");
 }

切面中也需要RequestInfo.instance().setPar()是因为MyArgumentResolver的参数注入只有在controller接口参数有加@Par注解才会生效,没有@Par参数时需要在切面中setPar()。

注册参数解析器

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

使用线程变量的好处

1、业务比较复杂时,可以减少方法的参数传递,确保方法只有一两个参数,使代码优雅起来。
只需要在线程结束时把线程变量clear掉 即可避免线程变量的泄漏

注意事项

若使用@RequestBody注解,则RequestUtil.getMap(request)无法获取数据,因为outPutStream只能读取一次。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值