Spring boot中自定義Json參數解析器的方法
一、介紹
用過springMVC/spring boot的都清楚,在controller層接受參數,常用的都是兩種接受方式,如下
/**
* 請求路徑 http://127.0.0.1:8080/test 提交類型為application/json
* 測試參數{"sid":1,"stuName":"里斯"}
* @param str
*/
@RequestMapping(value = "/test",method = RequestMethod.POST)
public void testJsonStr(@RequestBody(required = false) String str){
System.out.println(str);
}
/**
* 請求路徑 http://127.0.0.1:8080/testAcceptOrdinaryParam?str=123
* 測試參數
* @param str
*/
@RequestMapping(value = "/testAcceptOrdinaryParam",method = {RequestMethod.GET,RequestMethod.POST})
public void testAcceptOrdinaryParam(String str){
System.out.println(str);
}
第一個就是前端傳json參數,后臺使用RequestBody注解來接受參數。第二個就是普通的get/post提交數據,后臺進行接受參數的方式,當然spring還提供了參數在路徑中的解析格式等,這里不作討論
本文主要是圍繞前端解析Json參數展開,那@RequestBody既然能接受json參數,那它有什么缺點呢,
原spring 雖然提供了@RequestBody注解來封裝json數據,但局限性也挺大的,對參數要么適用jsonObject或者javabean類,或者string,
1、若使用jsonObject 接收,對于json里面的參數,還要進一步獲取解析,很麻煩
2、若使用javabean來接收,若接口參數不一樣,那么每一個接口都得對應一個javabean若使用string 來接收,那么也得需要自己解析json參數
3、所以琢磨了一個和get/post form-data提交方式一樣,直接在controller層接口寫參數名即可接收對應參數值。
重點來了,那么要完成在spring給controller層方法注入參數前,攔截這些參數,做一定改變,對于此,spring也提供了一個接口來讓開發(fā)者自己進行擴展。這個接口名為HandlerMethodArgumentResolver,它呢 是一個接口,它的作用主要是用來提供controller層參數攔截和注入用的。spring 也提供了很多實現類,這里不作討論,這里介紹它的一個比較特殊的實現類HandlerMethodArgumentResolverComposite,下面列出該類的一個實現方法
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException(
"Unsupported parameter type [" + parameter.getParameterType().getName() + "]." +
" supportsParameter should be called first.");
}
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
是不是感到比較驚訝,它自己不去執(zhí)行自己的resplveArgument方法,反而去執(zhí)行HandlerMethodArgumentResolver接口其他實現類的方法,具體原因,我不清楚,,,這個方法就是給controller層方法參數注入值得一個入口。具體的不多說啦!下面看代碼
二、實現步驟
要攔截一個參數,肯定得給這個參數一個標記,在攔截的時候,判斷有沒有這個標記,有則攔截,沒有則方向,這也是一種過濾器/攔截器原理,談到標記,那肯定非注解莫屬,于是一個注解類就產生了
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestJson {
/**
* 字段名,不填則默認參數名
* @return
*/
String fieldName() default "";
/**
* 默認值,不填則默認為null。
* @return
*/
String defaultValue() default "";
}
這個注解也不復雜,就兩個屬性,一個是fieldName,一個是defaultValue。有了這個,下一步肯定得寫該注解的解析器,而上面又談到HandlerMethodArgumentResolver接口可以攔截controller層參數,所以這個注解的解析器肯定得寫在該接口實現類里,
@Component
public class RequestJsonHandler implements HandlerMethodArgumentResolver {
/**
* json類型
*/
private static final String JSON_CONTENT_TYPE = "application/json";
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
//只有被reqeustJson注解標記的參數才能進入
return methodParameter.hasParameterAnnotation(RequestJson.class);
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
// 解析requestJson注解的代碼
}
一個大致模型搭建好了。要實現的初步效果,這里也說下,如圖
要去解析json參數,那肯定得有一些常用的轉換器,把json參數對應的值,轉換到controller層參數對應的類型中去,而常用的類型如 八種基本類型及其包裝類,String、Date類型,list/set,javabean等,所有可以先去定義一個轉換器接口。
public interface Converter {
/**
* 將value轉為clazz類型
* @param clazz
* @param value
* @return
*/
Object convert(Type clazz, Object value);
}
有了這個接口,那肯定得有幾個實現類,在這里,我將這些轉換器劃分為 ,7個陣營
1、Number類型轉換器,負責Byte/Integer/Float/Double/Long/Short 及基礎類型,還有BigInteger/BigDecimal兩個類
2、Date類型轉換器,負責日期類型
3、String類型轉換器,負責char及包裝類,還有string類型
4、Collection類型轉換器,負責集合類型
5、Boolean類型轉換器,負責boolean/Boolean類型
6、javaBean類型轉換器,負責普通的的pojo類
7、Map類型轉換器,負責Map接口
這里要需引入第三方包google,在文章末尾會貼出來。
代碼在這里就貼Number類型和Date類型,其余完整代碼,會在github上給出,地址 github鏈接
Number類型轉換器
public class NumberConverter implements Converter{
@Override
public Object convert(Type type, Object value){
Class<?> clazz = null;
if (!(type instanceof Class)){
return null;
}
clazz = (Class<?>) type;
if (clazz == null){
throw new RuntimeException("類型不能為空");
}else if (value == null){
return null;
}else if (value instanceof String && "".equals(String.valueOf(value))){
return null;
}else if (!clazz.isPrimitive() && clazz.getGenericSuperclass() != Number.class){
throw new ClassCastException(clazz.getTypeName() + "can not cast Number type!");
}
if (clazz == int.class || clazz == Integer.class){
return Integer.valueOf(String.valueOf(value));
}else if (clazz == short.class || clazz == Short.class){
return Short.valueOf(String.valueOf(value));
}else if (clazz == byte.class || clazz == Byte.class){
return Byte.valueOf(String.valueOf(value));
}else if (clazz == float.class || clazz == Float.class){
return Float.valueOf(String.valueOf(value));
}else if (clazz == double.class || clazz == Double.class){
return Double.valueOf(String.valueOf(value));
}else if (clazz == long.class || clazz == Long.class){
return Long.valueOf(String.valueOf(value));
}else if (clazz == BigDecimal.class){
return new BigDecimal(String.valueOf(value));
}else if (clazz == BigInteger.class){
return new BigDecimal(String.valueOf(value));
}else {
throw new RuntimeException("This type conversion is not supported!");
}
}
}
Date類型轉換器
/**
* 日期轉換器
* 對于日期校驗,這里只是簡單的做了一下,實際上還有對閏年的校驗,
* 每個月份的天數的校驗及其他日期格式的校驗
* @author: qiumin
* @create: 2018-12-30 10:43
**/
public class DateConverter implements Converter{
/**
* 校驗 yyyy-MM-dd HH:mm:ss
*/
private static final String REGEX_DATE_TIME = "^\\d{4}([-]\\d{2}){2}[ ]([0-1][0-9]|[2][0-4])(:[0-5][0-9]){2}$";
/**
* 校驗 yyyy-MM-dd
*/
private static final String REGEX_DATE = "^\\d{4}([-]\\d{2}){2}$";
/**
* 校驗HH:mm:ss
*/
private static final String REGEX_TIME = "^([0-1][0-9]|[2][0-4])(:[0-5][0-9]){2}";
/**
* 校驗 yyyy-MM-dd HH:mm
*/
private static final String REGEX_DATE_TIME_NOT_CONTAIN_SECOND = "^\\d{4}([-]\\d{2}){2}[ ]([0-1][0-9]|[2][0-4]):[0-5][0-9]$";
/**
* 默認格式
*/
private static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss";
/**
* 存儲數據map
*/
private static final Map<String,String> PATTERN_MAP = new ConcurrentHashMap<>();
static {
PATTERN_MAP.put(REGEX_DATE,"yyyy-MM-dd");
PATTERN_MAP.put(REGEX_DATE_TIME,"yyyy-MM-dd HH:mm:ss");
PATTERN_MAP.put(REGEX_TIME,"HH:mm:ss");
PATTERN_MAP.put(REGEX_DATE_TIME_NOT_CONTAIN_SECOND,"yyyy-MM-dd HH:mm");
}
@Override
public Object convert(Type clazz, Object value) {
if (clazz == null){
throw new RuntimeException("type must be not null!");
}
if (value == null){
return null;
}else if ("".equals(String.valueOf(value))){
return null;
}
try {
return new SimpleDateFormat(getDateStrPattern(String.valueOf(value))).parse(String.valueOf(value));
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
/**
* 獲取對應的日期字符串格式
* @param value
* @return
*/
private String getDateStrPattern(String value){
for (Map.Entry<String,String> m : PATTERN_MAP.entrySet()){
if (value.matches(m.getKey())){
return m.getValue();
}
}
return DEFAULT_PATTERN;
}
}
具體分析不做過多討論,詳情看代碼。
那寫完轉換器,那接下來,我們肯定要從request中拿到前端傳的參數,常用的獲取方式有request.getReader(),request.getInputStream(),但值得注意的是,這兩者者互斥。即在一次請求中使用了一者,然后另一個就獲取不到想要的結果。具體大家可以去試下。如果我們直接在解析requestJson注解的時候使用這兩個方法中的一個,那很大可能會出問題,因為我們也保證不了在spring中某個方法有使用到它,那肯定最好結果是不使用它或者包裝它(提前獲取getReader()/getInputStream()中的數據,將其存入一個byte數組,后續(xù)request使用這兩個方法獲取數據可以直接從byte數組中拿數據),不使用肯定不行,那得進一步去包裝它,在java ee中有提供這樣一個類HttpServletRequestWrapper,它就是httpsevletRequest的一個子實現類,也就是意味httpservletRequest的可以用這個來代替,具體大家可以去看看源碼,spring提供了幾個HttpServletRequestWrapper的子類,這里就不重復造輪子,這里使用ContentCachingRequestWrapper類。對request進行包裝,肯定得在filter中進行包裝
public class RequestJsonFilter implements Filter {
/**
* 用來對request中的Body數據進一步包裝
* @param req
* @param response
* @param chain
* @throws IOException
* @throws ServletException
*/
@Override
public void doFilter(ServletRequest req, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
if(req instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) req;
/**
* 只是為了防止一次請求中調用getReader(),getInputStream(),getParameter()
* 都清楚inputStream 并不具有重用功能,即多次讀取同一個inputStream流,
* 只有第一次讀取時才有數據,后面再次讀取inputStream 沒有數據,
* 即,getReader(),只能調用一次,但getParameter()可以調用多次,詳情可見ContentCachingRequestWrapper源碼
*/
requestWrapper = new ContentCachingRequestWrapper(request);
}
chain.doFilter(requestWrapper == null ? req : requestWrapper, response);
}
實現了過濾器,那肯定得把過濾器注冊到spring容器中,
@Configuration
@EnableWebMvc
public class WebConfigure implements WebMvcConfigurer {
@Autowired
private RequestJsonHandler requestJsonHandler;
// 把requestJson解析器也交給spring管理
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(0,requestJsonHandler);
}
@Bean
public FilterRegistrationBean filterRegister() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new RequestJsonFilter());
//攔截路徑
registration.addUrlPatterns("/");
//過濾器名稱
registration.setName("requestJsonFilter");
//是否自動注冊 false 取消Filter的自動注冊
registration.setEnabled(false);
//過濾器順序,需排在第一位
registration.setOrder(1);
return registration;
}
@Bean(name = "requestJsonFilter")
public Filter requestFilter(){
return new RequestJsonFilter();
}
}
萬事具備,就差解析器的代碼了。
對于前端參數的傳過來的json參數格式,大致有兩種。
一、{"name":"張三"}
二、[{"name":"張三"},{"name":"張三1"}]
所以解析的時候,要對這兩種情況分情況解析。
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
String contentType = request.getContentType();
// 不是json
if (!JSON_CONTENT_TYPE.equalsIgnoreCase(contentType)){
return null;
}
Object obj = request.getAttribute(Constant.REQUEST_BODY_DATA_NAME);
synchronized (RequestJsonHandler.class) {
if (obj == null) {
resolveRequestBody(request);
obj = request.getAttribute(Constant.REQUEST_BODY_DATA_NAME);
if (obj == null) {
return null;
}
}
}
RequestJson requestJson = methodParameter.getParameterAnnotation(RequestJson.class);
if (obj instanceof Map){
Map<String, String> map = (Map<String, String>)obj;
return dealWithMap(map,requestJson,methodParameter);
}else if (obj instanceof List){
List<Map<String,String>> list = (List<Map<String,String>>)obj;
return dealWithArray(list,requestJson,methodParameter);
}
return null;
}
/**
* 處理第一層json結構為數組結構的json串
* 這種結構默認就認為 為類似List<JavaBean> 結構,轉json即為List<Map<K,V>> 結構,
* 其余情況不作處理,若controller層為第一種,則數組里的json,轉為javabean結構,字段名要對應,
* 注意這里defaultValue不起作用
* @param list
* @param requestJson
* @param methodParameter
* @return
*/
private Object dealWithArray(List<Map<String,String>> list,RequestJson requestJson,MethodParameter methodParameter){
Class<?> parameterType = methodParameter.getParameterType();
return ConverterUtil.getConverter(parameterType).convert(methodParameter.getGenericParameterType(),JsonUtil.convertBeanToStr(list));
}
/**
* 處理{"":""}第一層json結構為map結構的json串,
* @param map
* @param requestJson
* @param methodParameter
* @return
*/
private Object dealWithMap(Map<String,String> map,RequestJson requestJson,MethodParameter methodParameter){
String fieldName = requestJson.fieldName();
if ("".equals(fieldName)){
fieldName = methodParameter.getParameterName();
}
Class<?> parameterType = methodParameter.getParameterType();
String orDefault = null;
if (map.containsKey(fieldName)){
orDefault = map.get(fieldName);
}else if (ConverterUtil.isMapType(parameterType)){
return map;
}else if (ConverterUtil.isBeanType(parameterType) || ConverterUtil.isCollectionType(parameterType)){
orDefault = JsonUtil.convertBeanToStr(map);
}else {
orDefault = map.getOrDefault(fieldName,requestJson.defaultValue());
}
return ConverterUtil.getConverter(parameterType).convert(methodParameter.getGenericParameterType(),orDefault);
}
/**
* 解析request中的body數據
* @param request
*/
private void resolveRequestBody(ServletRequest request){
BufferedReader reader = null;
try {
reader = request.getReader();
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
String parameterValues = sb.toString();
JsonParser parser = new JsonParser();
JsonElement element = parser.parse(parameterValues);
if (element.isJsonArray()){
List<Map<String,String>> list = new ArrayList<>();
list = JsonUtil.convertStrToBean(list.getClass(),parameterValues);
request.setAttribute(Constant.REQUEST_BODY_DATA_NAME, list);
}else {
Map<String, String> map = new HashMap<>();
map = JsonUtil.convertStrToBean(map.getClass(), parameterValues);
request.setAttribute(Constant.REQUEST_BODY_DATA_NAME, map);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if (reader != null){
try {
reader.close();
} catch (IOException e) {
// ignore
//e.printStackTrace();
}
}
}
}
整個代碼結構就是上面博文,完整代碼在github上,有感興趣的博友,可以看看地址 github鏈接,最后貼下maven依賴包
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.4</version> </dependency> </dependencies>
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
Java語言的Comparable和Comparator區(qū)別
這篇文章主要介紹了Java語言的Comparable和Comparator區(qū)別,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-06-06
SpringBoot+Redisson自定義注解一次解決重復提交問題
項目中經常會出現重復提交的問題,本文主要介紹了SpringBoot+Redisson自定義注解一次解決重復提交問題,具有一定的參考價值,感興趣的可以了解一下2024-03-03

