AOP、Http攔截器的使用

  • 切面的使用【自定義註解 AOP 攔截器】

我的開發過程中遇到大量的日誌輸出,統一處理方式,這種統一處理,或者日誌輸出,如果不使用切面等處理方式很容易造成代碼冗餘,看着很亂。
把一個個的橫切關注點放到某個模塊中去,稱之爲切面。那麼每一個的切面都能影響業務的某一種功能,切面的目的就是功能增強,如日誌切面就是一個橫切關注點,應用中許多方法需要日誌記錄的只需要插入日誌的切面即可。
AOP術語
Joinpoint:連接點,被攔截到需要被增強的方法。

Pointcut:切入點,哪些包中的哪些類中的哪些方法,可認爲是連接點的集合。

Advice:增強,當攔截到Joinpoint之後,在方法執行的什麼時機(when)做什麼樣(what)的增強。根據時機分爲:前置增強、後置增強、異常增強、最終增強、環繞增強

Aspect:切面,Pointcut+Advice,去哪些地方+在什麼時機+做什麼增強

Target:目標對象,被代理的目標對象

weaving:織入,把Advice加到Target上之後,創建出Proxy對象的過程

Proxy:一個類被AOP織入增強後,產生的代理類

創建切面

@Aspect
@Component
public class AspectDoPost {

	//切面位置:所有controller下的請求【一般打印日誌使用】
    @Pointcut("execution(* com.jingchuang.jcos.business.controller.*.*(..))")
    public void log() {
    }
    
    //切面 配置通知
    @Before("log()")         //AfterReturning
    public void before(JoinPoint joinPoint) {
			//從JoinPoint裏可以獲取到很多東西
			MethodSignature signature = (MethodSignature) joinPoint.getSignature();
			//獲取切入點所在的方法
        	Method method = signature.getMethod();
			HttpServletRequest request = 
			((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
	}
	
    @AfterReturning(returning = "object", pointcut = "log()")
    public void adAfterReturning(Object object) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        log.info("resopnse=" + JSON.toJSONString(object));
        log.info("==============================end==============================");
    }



    /**
     * 切面位置:自定義註解
     */
    @Pointcut(value = "@annotation(com.jingchuang.jcos.business.annotation.MyRetryableMethod)")
    public void doPostLog() {
    }

    @Around(value = "doPostLog()")
    public Object aroundMethod(ProceedingJoinPoint pjd){
		//ProceedingJoinPoint 也可以基於反射獲取很多信息
    }
}

ProceedingJoinPoint 接口類源碼中有兩個抽象方法;

package org.aspectj.lang;

import org.aspectj.runtime.internal.AroundClosure;

public interface ProceedingJoinPoint extends JoinPoint {
    void set$AroundClosure(AroundClosure var1);

    Object proceed() throws Throwable;

    Object proceed(Object[] var1) throws Throwable;
}

環繞通知 ProceedingJoinPoint 執行proceed方法的作用是讓目標方法執行,這也是環繞通知和前置、後置通知方法的一個最大區別。環繞通知=前置+目標方法執行+後置通知,proceed方法就是用於啓動目標方法執行的。

使用切面對Controller層攔截時需要注意

//如果程序有統一返回異常處理的話,並且也在com.**.**.**controller包下這裏的父類也是會被切面執行的。
public class LoginController extends BaseController {}

攔截器

攔截器的實現可以通過實現接口HandlerInterceptor。
還是先看源碼

package org.springframework.web.servlet;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;

public interface HandlerInterceptor {
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }

    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }

    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
    }
}

從上述源碼我們可以看到 三個default修飾的接口,三個接口爲:
前置處理
preHandle
前置處理返回值爲boolean類型,如果返回true,我們可以進入正常的應用,如果返回false,則攔截器目的達到了,攔截掉不應該進入到程序的請求。
後置處理
完成處理
案例:通過攔截器實現對請求參數校驗數據。【字段有沒有傳入】
前提:對接外部數據時,對方是主系統(並未對拋出的異常做處理),我們更希望自己能獲取到所有信息,所以請求參數並沒有使用@RequestBody去匹配對應的json,或者實體類,因爲如果對方傳入數據不是json格式,將會拋出運行時異常,而數據沒有進入到自己的程序中。
實現設計:使用攔截器,過濾非法數據,用IO讀取body數據(當讀取數據時如果不寫會,正常程序無法再次讀取到數據【下標】這裏不說原因了,直說解決方式),使用自定義註解實現接口需要驗證哪些數據。
一、創建自定義註解,並且定義成運行時生效,Element類型爲Method

/**需要驗證的參數,isEmptyJson使用String類型也可以*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface JsonCheck {

    /**需要校驗哪些參數是需要驗證爲空*/
    public String[] isEmptyJson()  ;

    /**需要校驗那些參數是需要校驗爲空的trim後*/
    public String[] isBlankJson() default "";

    /**方法名稱*/
    public String methodName() default "" ;

    /**錯誤異常拋出樣式*/
    public String myException() default "";

}

二、配置攔截的資源

@Configuration
public class ApplicationConfig implements WebMvcConfigurer {

	/**實現攔截*/
    @Autowired
    private JsonCheckInterceptor jsonCheckInterceptor;

    /**
     * 配置攔截器
     *
     * @param registry
     * @author lance
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //json參數攔截
        registry.addInterceptor(jsonCheckInterceptor)
                .addPathPatterns("/lmsApi/LMSCancelReport")
                .addPathPatterns("/lmsApi/MoveStoreEndFromLMS")
                .addPathPatterns("/lmsApi/GetTruckStoreEndStatusFromLMS")
                .excludePathPatterns("/static/**", "/login.html");
    }
}

三、創建BodyReaderHttpServletRequestWrapper
如果不需要從Body裏面讀取數據,請忽略這一塊

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;

/**
 * @Author 陳子燁
 * @Date 2019/12/30 12:52
 * @Version 1.0
 **/
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {

    public final String body;

    /**
     *
     * @param request
     */
    public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException{
        super(request);
        StringBuilder sb = new StringBuilder();
        InputStream ins = request.getInputStream();
        BufferedReader isr = null;
        try{
            if(ins != null){
                isr = new BufferedReader(new InputStreamReader(ins));
                char[] charBuffer = new char[128];
                int readCount = 0;
                while((readCount = isr.read(charBuffer)) != -1){
                    sb.append(charBuffer,0,readCount);
                }
            }else{
                sb.append("");
            }
        }catch (IOException e){
            throw e;
        }finally {
            if(isr != null) {
                isr.close();
            }
        }

        sb.toString();
        body = sb.toString();
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream byteArrayIns = new ByteArrayInputStream(body.getBytes());
        ServletInputStream servletIns = 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 byteArrayIns.read();
            }
        };
        return  servletIns;
    }

}

四、攔截器實現

@Component
public class JsonCheckInterceptor implements HandlerInterceptor {

	@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        try{
            if (handler instanceof HandlerMethod) {
                // 強轉
                HandlerMethod handlerMethod = (HandlerMethod) handler;
                // 獲取方法
                Method method = handlerMethod.getMethod();
                // 是否有JsonCheck註解
                if (!method.isAnnotationPresent(JsonCheck.class)) {
                    return true;
                }
                // 獲取註解內容信息
                JsonCheck jsonCheck = method.getAnnotation(JsonCheck.class);
                if (jsonCheck == null) {
                    return true;
                }

                //開始校驗參數 並且封裝返回
                String[] isEmptyJson = jsonCheck.isEmptyJson();
                BodyReaderHttpServletRequestWrapper bodyReader = new BodyReaderHttpServletRequestWrapper(request);
                String param = bodyReader.body;
                JSONObject jsonParam = JSON.parseObject(param);
                StringBuffer sb = new StringBuffer();
                boolean bool = true;
                lmsResult result = new lmsResult();
                for(String s:isEmptyJson){
                    if(MyStringUtils.isEmpty(jsonParam.getString(s))){
                        sb.append(jsonCheck.myException()+s+"異常;");
                        bool = false;
                    }
                }
                //可以通過註解來實現一些別的
                //如果是false 插入一條日誌。記錄返回值
                if(!bool){
                    responseOut(response,result);
                    return bool;
                }

            }
        }catch (Exception e){
        	//自定義處理異常,返回自己寫的MyException
            logger.info("攔截驗證");
            e.printStackTrace();
        }
        return true;

	}


    /**
     * @param response : 響應請求
     * @param object:  object
     * @return void
     * @Title: out
     * @Description: response輸出JSON數據
     **/
    private static void responseOut(ServletResponse response, Object object) {

        try (PrintWriter out =  response.getWriter()){
            response.setContentType("application/json;charset=UTF-8");
            response.setCharacterEncoding("UTF-8");
            out.println(JSONObject.toJSONString(object));
        } catch (Exception e) {
            logger.error("響應出錯:{}", e);
            e.printStackTrace();
        }
    }

    private String getIpAddr(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) {
            //多次反向代理後會有多個ip值,第一個ip纔是真實ip
            int index = ip.indexOf(",");
            if (index != -1) {
                return ip.substring(0, index);
            } else {
                return ip;
            }
        }
        ip = request.getHeader("X-Real-IP");
        if (StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) {
            return ip;
        }
        return request.getRemoteAddr();
    }
}

五、post接口

    @PostMapping(value = "/GetTruckStoreEndStatusFromLMS")
    @ResponseBody
    @JsonCheck(isEmptyJson = {"companyId","storeName","storeCode","time","token","carMark","mainProdListNo","scheduleNo"},
            methodName = "GetTruckStoreEndStatusFromLMS"
    )
    public Result test(String param){
	}

其他:校驗參數是否爲空,是否特定格式,還是使用官方註解比較好~這種情況只是特殊業務需要我自己寫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章