Spring boot中自定義Json引數解析器
轉載請註明出處。。。
一、介紹
用過springMVC/spring boot的都清楚,在controller層接受引數,常用的都是兩種接受方式,如下
1 /** 2 * 請求路徑 http://127.0.0.1:8080/test 提交型別為application/json 3 * 測試引數{"sid":1,"stuName":"里斯"} 4 * @param str 5 */ 6 @RequestMapping(value = "/test",method = RequestMethod.POST) 7 public void testJsonStr(@RequestBody(required = false) String str){ 8 System.out.println(str); 9 } 10 /** 11 * 請求路徑 http://127.0.0.1:8080/testAcceptOrdinaryParam?str=123 12 * 測試引數 13 * @param str 14 */ 15 @RequestMapping(value = "/testAcceptOrdinaryParam",method = {RequestMethod.GET,RequestMethod.POST}) 16 public voidtestAcceptOrdinaryParam(String str){ 17 System.out.println(str); 18 }
第一個就是前端傳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也提供了一個介面來讓開發者自己進行擴充套件。這個介面名為HandlerMethodArgumentResolver,它呢 是一個介面,它的作用主要是用來提供controller層引數攔截和注入用的。spring 也提供了很多實現類,這裡不作討論,這裡介紹它的一個比較特殊的實現類HandlerMethodArgumentResolverComposite,下面列出該類的一個實現方法
1 @Override 2 @Nullable 3 public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, 4 NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { 5 6 HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); 7 if (resolver == null) { 8 throw new IllegalArgumentException( 9 "Unsupported parameter type [" + parameter.getParameterType().getName() + "]." + 10 " supportsParameter should be called first."); 11 } 12 return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); 13 }
是不是感到比較驚訝,它自己不去執行自己的resplveArgument方法,反而去執行HandlerMethodArgumentResolver介面其他實現類的方法,具體原因,我不清楚,,,這個方法就是給controller層方法引數注入值得一個入口。具體的不多說啦!下面看程式碼
二、實現步驟
要攔截一個引數,肯定得給這個引數一個標記,在攔截的時候,判斷有沒有這個標記,有則攔截,沒有則方向,這也是一種過濾器/攔截器原理,談到標記,那肯定非註解莫屬,於是一個註解類就產生了
1 @Target(ElementType.PARAMETER) 2 @Retention(RetentionPolicy.RUNTIME) 3 public @interface RequestJson { 4 5 /** 6 * 欄位名,不填則預設引數名 7 * @return 8 */ 9 String fieldName() default ""; 10 11 /** 12 * 預設值,不填則預設為null。 13 * @return 14 */ 15 String defaultValue() default ""; 16 }
這個註解也不復雜,就兩個屬性,一個是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等,所有可以先去定義一個轉換器介面。
1 public interface Converter { 2 3 /** 4 * 將value轉為clazz型別 5 * @param clazz 6 * @param value 7 * @return 8 */ 9 Object convert(Type clazz, Object value); 10 }
有了這個介面,那肯定得有幾個實現類,在這裡,我將這些轉換器劃分為 ,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型別轉換器
1 public class NumberConverter implements Converter{ 2 3 @Override 4 public Object convert(Type type, Object value){ 5 Class<?> clazz = null; 6 if (!(type instanceof Class)){ 7 return null; 8 } 9 clazz = (Class<?>) type; 10 if (clazz == null){ 11 throw new RuntimeException("型別不能為空"); 12 }else if (value == null){ 13 return null; 14 }else if (value instanceof String && "".equals(String.valueOf(value))){ 15 return null; 16 }else if (!clazz.isPrimitive() && clazz.getGenericSuperclass() != Number.class){ 17 throw new ClassCastException(clazz.getTypeName() + "can not cast Number type!"); 18 } 19 if (clazz == int.class || clazz == Integer.class){ 20 return Integer.valueOf(String.valueOf(value)); 21 }else if (clazz == short.class || clazz == Short.class){ 22 return Short.valueOf(String.valueOf(value)); 23 }else if (clazz == byte.class || clazz == Byte.class){ 24 return Byte.valueOf(String.valueOf(value)); 25 }else if (clazz == float.class || clazz == Float.class){ 26 return Float.valueOf(String.valueOf(value)); 27 }else if (clazz == double.class || clazz == Double.class){ 28 return Double.valueOf(String.valueOf(value)); 29 }else if (clazz == long.class || clazz == Long.class){ 30 return Long.valueOf(String.valueOf(value)); 31 }else if (clazz == BigDecimal.class){ 32 return new BigDecimal(String.valueOf(value)); 33 }else if (clazz == BigInteger.class){ 34 return new BigDecimal(String.valueOf(value)); 35 }else { 36 throw new RuntimeException("This type conversion is not supported!"); 37 } 38 } 39 40 41 }
Date型別轉換器
1 /** 2 * 日期轉換器 3 * 對於日期校驗,這裡只是簡單的做了一下,實際上還有對閏年的校驗, 4 * 每個月份的天數的校驗及其他日期格式的校驗 5 * @author: qiumin 6 * @create: 2018-12-30 10:43 7 **/ 8 public class DateConverter implements Converter{ 9 10 /** 11 * 校驗 yyyy-MM-dd HH:mm:ss 12 */ 13 private static final String REGEX_DATE_TIME = "^\\d{4}([-]\\d{2}){2}[ ]([0-1][0-9]|[2][0-4])(:[0-5][0-9]){2}$"; 14 15 /** 16 * 校驗 yyyy-MM-dd 17 */ 18 private static final String REGEX_DATE = "^\\d{4}([-]\\d{2}){2}$"; 19 20 /** 21 * 校驗HH:mm:ss 22 */ 23 private static final String REGEX_TIME = "^([0-1][0-9]|[2][0-4])(:[0-5][0-9]){2}"; 24 25 /** 26 * 校驗 yyyy-MM-dd HH:mm 27 */ 28 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]$"; 29 30 /** 31 * 預設格式 32 */ 33 private static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss"; 34 35 36 /** 37 * 儲存資料map 38 */ 39 private static final Map<String,String> PATTERN_MAP = new ConcurrentHashMap<>(); 40 41 static { 42 PATTERN_MAP.put(REGEX_DATE,"yyyy-MM-dd"); 43 PATTERN_MAP.put(REGEX_DATE_TIME,"yyyy-MM-dd HH:mm:ss"); 44 PATTERN_MAP.put(REGEX_TIME,"HH:mm:ss"); 45 PATTERN_MAP.put(REGEX_DATE_TIME_NOT_CONTAIN_SECOND,"yyyy-MM-dd HH:mm"); 46 } 47 48 @Override 49 public Object convert(Type clazz, Object value) { 50 if (clazz == null){ 51 throw new RuntimeException("type must be not null!"); 52 } 53 if (value == null){ 54 return null; 55 }else if ("".equals(String.valueOf(value))){ 56 return null; 57 } 58 try { 59 return new SimpleDateFormat(getDateStrPattern(String.valueOf(value))).parse(String.valueOf(value)); 60 } catch (ParseException e) { 61 throw new RuntimeException(e); 62 } 63 } 64 65 /** 66 * 獲取對應的日期字串格式 67 * @param value 68 * @return 69 */ 70 private String getDateStrPattern(String value){ 71 for (Map.Entry<String,String> m : PATTERN_MAP.entrySet()){ 72 if (value.matches(m.getKey())){ 73 return m.getValue(); 74 } 75 } 76 return DEFAULT_PATTERN; 77 } 78 }
具體分析不做過多討論,詳情看程式碼。
那寫完轉換器,那接下來,我們肯定要從request中拿到前端傳的引數,常用的獲取方式有request.getReader(),request.getInputStream(),但值得注意的是,這兩者者互斥。即在一次請求中使用了一者,然後另一個就獲取不到想要的結果。具體大家可以去試下。如果我們直接在解析requestJson註解的時候使用這兩個方法中的一個,那很大可能會出問題,因為我們也保證不了在spring中某個方法有使用到它,那肯定最好結果是不使用它或者包裝它(提前獲取getReader()/getInputStream()中的資料,將其存入一個byte陣列,後續request使用這兩個方法獲取資料可以直接從byte陣列中拿資料),不使用肯定不行,那得進一步去包裝它,在java ee中有提供這樣一個類HttpServletRequestWrapper,它就是httpsevletRequest的一個子實現類,也就是意味httpservletRequest的可以用這個來代替,具體大家可以去看看原始碼,spring提供了幾個HttpServletRequestWrapper的子類,這裡就不重複造輪子,這裡使用ContentCachingRequestWrapper類。對request進行包裝,肯定得在filter中進行包裝
1 public class RequestJsonFilter implements Filter { 2 3 4 /** 5 * 用來對request中的Body資料進一步包裝 6 * @param req 7 * @param response 8 * @param chain 9 * @throws IOException 10 * @throws ServletException 11 */ 12 @Override 13 public void doFilter(ServletRequest req, ServletResponse response, FilterChain chain) throws IOException, ServletException { 14 ServletRequest requestWrapper = null; 15 if(req instanceof HttpServletRequest) { 16 HttpServletRequest request = (HttpServletRequest) req; 17 /** 18 * 只是為了防止一次請求中呼叫getReader(),getInputStream(),getParameter() 19 * 都清楚inputStream 並不具有重用功能,即多次讀取同一個inputStream流, 20 * 只有第一次讀取時才有資料,後面再次讀取inputStream 沒有資料, 21 * 即,getReader(),只能呼叫一次,但getParameter()可以呼叫多次,詳情可見ContentCachingRequestWrapper原始碼 22 */ 23 requestWrapper = new ContentCachingRequestWrapper(request); 24 } 25 chain.doFilter(requestWrapper == null ? req : requestWrapper, response); 26 }
實現了過濾器,那肯定得把過濾器註冊到spring容器中,
1 @Configuration 2 @EnableWebMvc 3 public class WebConfigure implements WebMvcConfigurer { 4 5 6 @Autowired 7 private RequestJsonHandler requestJsonHandler; 8 9 // 把requestJson解析器也交給spring管理 10 @Override 11 public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { 12 resolvers.add(0,requestJsonHandler); 13 } 14 15 @Bean 16 public FilterRegistrationBean filterRegister() { 17 FilterRegistrationBean registration = new FilterRegistrationBean(); 18 registration.setFilter(new RequestJsonFilter()); 19 //攔截路徑 20 registration.addUrlPatterns("/"); 21 //過濾器名稱 22 registration.setName("requestJsonFilter"); 23 //是否自動註冊 false 取消Filter的自動註冊 24 registration.setEnabled(false); 25 //過濾器順序,需排在第一位 26 registration.setOrder(1); 27 return registration; 28 } 29 30 @Bean(name = "requestJsonFilter") 31 public Filter requestFilter(){ 32 return new RequestJsonFilter(); 33 } 34 }
萬事具備,就差解析器的程式碼了。
對於前端引數的傳過來的json引數格式,大致有兩種。
一、{"name":"張三"}
二、[{"name":"張三"},{"name":"張三1"}]
所以解析的時候,要對這兩種情況分情況解析。
1 @Override 2 public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception { 3 4 HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class); 5 String contentType = request.getContentType(); 6 // 不是json 7 if (!JSON_CONTENT_TYPE.equalsIgnoreCase(contentType)){ 8 return null; 9 } 10 Object obj = request.getAttribute(Constant.REQUEST_BODY_DATA_NAME); 11 synchronized (RequestJsonHandler.class) { 12 if (obj == null) { 13 resolveRequestBody(request); 14 obj = request.getAttribute(Constant.REQUEST_BODY_DATA_NAME); 15 if (obj == null) { 16 return null; 17 } 18 } 19 } 20 RequestJson requestJson = methodParameter.getParameterAnnotation(RequestJson.class); 21 if (obj instanceof Map){ 22 Map<String, String> map = (Map<String, String>)obj; 23 return dealWithMap(map,requestJson,methodParameter); 24 }else if (obj instanceof List){ 25 List<Map<String,String>> list = (List<Map<String,String>>)obj; 26 return dealWithArray(list,requestJson,methodParameter); 27 } 28 return null; 29 } 30 31 /** 32 * 處理第一層json結構為陣列結構的json串 33 * 這種結構預設就認為 為類似List<JavaBean> 結構,轉json即為List<Map<K,V>> 結構, 34 * 其餘情況不作處理,若controller層為第一種,則數組裡的json,轉為javabean結構,欄位名要對應, 35 * 注意這裡defaultValue不起作用 36 * @param list 37 * @param requestJson 38 * @param methodParameter 39 * @return 40 */ 41 private Object dealWithArray(List<Map<String,String>> list,RequestJson requestJson,MethodParameter methodParameter){ 42 Class<?> parameterType = methodParameter.getParameterType(); 43 return ConverterUtil.getConverter(parameterType).convert(methodParameter.getGenericParameterType(),JsonUtil.convertBeanToStr(list)); 44 } 45 /** 46 * 處理{"":""}第一層json結構為map結構的json串, 47 * @param map 48 * @param requestJson 49 * @param methodParameter 50 * @return 51 */ 52 private Object dealWithMap(Map<String,String> map,RequestJson requestJson,MethodParameter methodParameter){ 53 String fieldName = requestJson.fieldName(); 54 if ("".equals(fieldName)){ 55 fieldName = methodParameter.getParameterName(); 56 } 57 Class<?> parameterType = methodParameter.getParameterType(); 58 String orDefault = null; 59 if (map.containsKey(fieldName)){ 60 orDefault = map.get(fieldName); 61 }else if (ConverterUtil.isMapType(parameterType)){ 62 return map; 63 }else if (ConverterUtil.isBeanType(parameterType) || ConverterUtil.isCollectionType(parameterType)){ 64 orDefault = JsonUtil.convertBeanToStr(map); 65 }else { 66 orDefault = map.getOrDefault(fieldName,requestJson.defaultValue()); 67 } 68 return ConverterUtil.getConverter(parameterType).convert(methodParameter.getGenericParameterType(),orDefault); 69 } 70 71 /** 72 * 解析request中的body資料 73 * @param request 74 */ 75 private void resolveRequestBody(ServletRequest request){ 76 BufferedReader reader = null; 77 try { 78 reader = request.getReader(); 79 StringBuilder sb = new StringBuilder(); 80 String line = null; 81 while ((line = reader.readLine()) != null) { 82 sb.append(line); 83 } 84 String parameterValues = sb.toString(); 85 JsonParser parser = new JsonParser(); 86 JsonElement element = parser.parse(parameterValues); 87 if (element.isJsonArray()){ 88 List<Map<String,String>> list = new ArrayList<>(); 89 list = JsonUtil.convertStrToBean(list.getClass(),parameterValues); 90 request.setAttribute(Constant.REQUEST_BODY_DATA_NAME, list); 91 }else { 92 Map<String, String> map = new HashMap<>(); 93 map = JsonUtil.convertStrToBean(map.getClass(), parameterValues); 94 request.setAttribute(Constant.REQUEST_BODY_DATA_NAME, map); 95 } 96 } catch (IOException e) { 97 e.printStackTrace(); 98 }finally { 99 if (reader != null){ 100 try { 101 reader.close(); 102 } catch (IOException e) { 103 // ignore 104 //e.printStackTrace(); 105 } 106 } 107 } 108 }
整個程式碼結構就是上面博文,完整程式碼在github上,有感興趣的博友,可以看看地址 github連結,最後貼下maven依賴包
1 <dependencies> 2 <dependency> 3 <groupId>org.springframework.boot</groupId> 4 <artifactId>spring-boot-starter-web</artifactId> 5 </dependency> 6 7 <dependency> 8 <groupId>org.springframework.boot</groupId> 9 <artifactId>spring-boot-starter-tomcat</artifactId> 10 <scope>provided</scope> 11 </dependency> 12 <dependency> 13 <groupId>org.springframework.boot</groupId> 14 <artifactId>spring-boot-starter-test</artifactId> 15 <scope>test</scope> 16 </dependency> 17 <dependency> 18 <groupId>com.google.code.gson</groupId> 19 <artifactId>gson</artifactId> 20 <version>2.8.4</version> 21 </dependency> 22 </dependencies>
----------------------------------------------------------------------------------------------------華麗的分界線------------------------------------------------------------------------------------------------------------
以後就是本文全部內容,若有不足或錯誤之處還望指正,謝謝!