<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
問題源頭:
在日常的開發中,在Service層經常會用到對某一些必填引數進行是否存在的校驗。比如我在寫一個專案管理系統:
這種必填引數少一些還好,如果多一些的話光是if語句就要寫一堆。像我這種有程式碼潔癖的人看著這一堆無用程式碼更是難受。
如何解決:
在Spring裡面有一個非常好用的東西可以對方法進行增強,那就是AOP。AOP可以對方法進行增強,比如:我要校驗引數是否存在,可以在執行這個方法之前對請求裡面的引數進行校驗判斷是否存在,如果不存在就直接的丟擲異常。
因為不是所有的方法都需要進行必填引數的校驗,所以我還需要一個標識用來標記需要校驗引數的方法,這個標記只能標記在方法上。這一部分的功能可以使用Java中的註解來實現。然後配合AOP來實現必填引數的校驗。
程式碼實現:
這個是標記註解的程式碼:
package com.gcs.demo.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface CheckRequireParam { String[] requireParam() default ""; }
@Target({ElementType.METHOD}):作用是該註解只能用到方法上
@Retention(RetentionPolicy.RUNTIME):註解不僅被保留到 class 檔案中,JVM 載入 class 檔案之後,會仍然存在
這個裡面還有一個requireParam引數,用來存放必填引數的Key
需要依賴的Jar:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>版本號</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>版本號</version> </dependency>
因為這裡是要在執行一個方法之前對傳入的引數進行校驗,所以這裡使用到了AOP的環繞通知
AOP裡面的通知方式:
這裡我選用的是環繞通知,環繞通知是這幾個通知中最強大的一個功能。我選擇環繞通知的一個原因是,環繞通知可以通過程式碼來控制被代理方法是否執行。
現在需要建立一個切面類,並且該類需要被@Aspect
和@Component
標記:
@Component @Aspect public class CheckRequireParamAop { //.....do something }
這個類裡面加了一個方法有來設定切點,通過@Pointcut
註解
@Pointcut:這個引數是一個表示式,其作用是用來指定哪些方法需要被"增強"
@Pointcut("@annotation(com.gcs.demo.annotation.CheckRequireParam)") public void insertPoint(){ }
接下來就是要寫一個增強的方法,因為我是選用的環繞通知,所以該方法需要被@Around
標記
@Around("insertPoint()") public Object checkParam(ProceedingJoinPoint proceedingJoinPoint){ //.....do something }
然後就要具體的來聊一下這個checkParam
方法裡面要做什麼事情了。
首先,這個的功能是校驗引數,那麼首先要做的是將請求的引數獲取到。這裡獲取引數的方式就要區分成GET
和POST
請求。GET請求還好可以通過HttpServletRequest
物件裡面的getParameterMap
方法可以直接獲取到,然而POST
通過這個方法就不可以了。
public Map<String,String> getRequestParams(HttpServletRequest request) throws IOException { Map<String,String> resultParam = null; if(request.getMethod().equalsIgnoreCase("POST")){ StringBuffer data = new StringBuffer(); String line = null; BufferedReader reader = request.getReader(); while (null != (line = reader.readLine())) data.append(line); if(data.length() != 0) { resultParam = JSONObject.parseObject(data.toString(), new TypeReference<Map<String,String>>(){}); } }else if(request.getMethod().equalsIgnoreCase("GET")){ resultParam = request.getParameterMap().entrySet().stream().collect(Collectors.toMap(i -> i.getKey(), e -> Arrays.stream(e.getValue()).collect(Collectors.joining(",")))); } return resultParam != null ? resultParam : new HashMap(); }
這裡通過if分成了兩塊:
POST
GET
獲取到引數後就可以對引數進行校驗是否存在了:
@Around("insertPoint()") public Object checkParam(ProceedingJoinPoint proceedingJoinPoint){ //獲取到HttpServletRequest物件 HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); MethodSignature signature = (MethodSignature)proceedingJoinPoint.getSignature(); //獲取到CheckRequireParam註解 CheckRequireParam annotation = signature.getMethod().getAnnotation(CheckRequireParam.class); //獲取到CheckRequireParam註解中的requireParam屬性 String[] checkParams = annotation.requireParam(); try { //通過封裝的方法獲取到請求的引數 Map<String,String> parameterMap = getRequestParams(request); //當規定了必傳引數,獲取到的引數裡面是空的,這裡就直接丟擲異常 if(checkParams.length > 0 && (parameterMap == null || parameterMap.size() == 0)){ throw new ParamNotRequire("當前獲取到的引數為空"); } //通過迴圈判斷requireParam中的屬性名是否在請求引數的中是否存在 Arrays.stream(checkParams).forEach(item ->{ if(!parameterMap.containsKey(item)){ throw new ParamNotRequire("引數[" + item + "]不存在"); } if(!StringUtils.hasLength(parameterMap.get(item))){ throw new ParamNotRequire("引數[" + item + "]不能為空"); } }); //這個proceed方法一定要進行呼叫,否則走不到代理的方法 Object proceed = proceedingJoinPoint.proceed(); return proceed; } catch (Throwable throwable) { //如果引數不存在會丟擲ParamNotRequire異常會被這裡捕獲到,在這裡重新將其丟擲,讓全域性例外處理器進行處理 if(throwable instanceof ParamNotRequire){ throw (ParamNotRequire)throwable; } throwable.printStackTrace(); } return null; }
上面的程式碼總結下大概有以下幾步:
程式碼寫到這裡,你建立一個Controller,然後寫一個Get方法,程式應該是正常執行的,並且可以判斷出哪一個引數沒有傳值。
建立Controller是很簡單的,這裡我只貼出測試要用的程式碼:
@GetMapping("/test") @CheckRequireParam(requireParam = {"username","age"}) public String testRequireParam(UserInfo info){ return info.getUsername(); }
把引數按照CheckRequireParam註解的規定傳入是可以正常返回沒有丟擲異常:
將age引數刪除掉,就丟擲了引數不存在的異常:
Get請求測試完美,撒花!!!!!
寫一個測試的方法:
@PostMapping("/postTest") @CheckRequireParam(requireParam = {"password"}) public UserInfo postTest(@RequestBody UserInfo userInfo){ return userInfo; }
存取後並沒有給出對應的錯誤資訊,不過看後臺是出現了非法狀態異常:
這個問題的原因是,在使用@RequestBody的時候,它會通過流的方式將資料讀出來(getReader或getInputStream),而這種方式讀取資料只能讀取一次,不能讀取第二次。
這裡我解決這一問題的方法是先將RequestBody儲存為一個byte陣列,然後繼承HttpServletRequestWrapper類覆蓋getReader()和getInputStream()方法,使流從儲存的byte陣列讀取。
繼承HttpServletRequestWrapper類重寫getInputStream和getReader方法,每次讀的時候讀取儲存在requestBody中的資料
public class CustomRequestWrapper extends HttpServletRequestWrapper { private byte[] requestBody; private HttpServletRequest request; public RequestWrapper(HttpServletRequest request) { super(request); this.request = request; } @Override public ServletInputStream getInputStream() throws IOException { if(this.requestBody == null){ ByteArrayOutputStream bos = new ByteArrayOutputStream(); IOUtils.copy(request.getInputStream(),bos); this.requestBody = bos.toByteArray(); } ByteArrayInputStream bis = new ByteArrayInputStream(requestBody); return new ServletInputStream() { @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } @Override public int read() throws IOException { return bis.read(); } }; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(this.getInputStream())); } }
增加一個過濾器,把Filter中的ServletRequest替換為ServletRequestWrapper
@Component @WebFilter(filterName = "channelFilter",urlPatterns = {"/*"}) public class CustomFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { ServletRequest requestWrapper = null; if(request instanceof HttpServletRequest){ requestWrapper = new CustomRequestWrapper((HttpServletRequest) request); } if(requestWrapper == null){ filterChain.doFilter(request,servletResponse); }else{ filterChain.doFilter(requestWrapper,servletResponse); } } }
按照CheckRequireParam規則傳入引數:
不傳入引數獲者傳入一個空的引數:
到此這篇關於SpringBoot通過AOP與註解實現入參校驗詳情的文章就介紹到這了,更多相關SpringBoot入參校驗內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45