- 切面的使用【自定义注解 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){
}
其他:校验参数是否为空,是否特定格式,还是使用官方注解比较好~这种情况只是特殊业务需要我自己写。