- 切面的使用【自定義註解 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){
}
其他:校驗參數是否爲空,是否特定格式,還是使用官方註解比較好~這種情況只是特殊業務需要我自己寫。