面试过程中,经常会问别人AOP,或者作为被面试者,被别人问AOP。AOP是什么,面向切面编程,都能扯上个一二三来。AOP的概念已经烂大街了,搞后端开发的都知道。
一旦问道你有没有用到过AOP,或者自己实现AOP,大部分面试者的答案都很标准:
- 打印日志有用过,再追问什么日志,支支吾吾说不清楚。
- 事务用过,事务spring已经给你设计、封装的明明白白,和你有什么关系。
所以只要回答这两种答案,这个知识点就不会给分了。
我理解在开发过程中常用的AOP有三种,过滤器、拦截器、切面注解,针对这三个东西,分别写一下在我自己搭建的框架中的一些用法。
一、过滤器
过滤器其实不是springboot提供的组件或特性,我们在搞jsp、servlet的时候就经常会用到过滤器。关于过滤器和拦截器的关系、区别,就不介绍了,网上很多很好的文章,我认为,使用了springboot,绝大多数场景都应该使用拦截器,因为拦截器可以获取到spring管理的bean,过滤器就不行。什么时候使用过滤器呢,从网上抄代码,正好他用的过滤器。
介绍一个我的框架中使用的过滤器,就是输出日志,日志的内容是打印request的请求地址、参数,response的返回值。这个过滤器的使用又恰恰使用的spring提供的过滤器,嘲讽啊,直接上代码。
1、定义过滤器
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collection;
@Slf4j
public class LoggingFilter extends OncePerRequestFilter {
private static final String REQUEST_PREFIX_NAME = "Request: ";
private static final String RESPONSE_PREFIX_NAME = "Response: ";
private static final int MAXLENGTH = 1000;
private static final String ATTACH_RESPONSE_HEADER = "Content-disposition";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 包装Request 和 Response
//request = new RequestWrapper(request);
response = new ResponseWrapper(response);
// 打印请求参数
request = printRequestLog(request);
// 执行过滤器链
filterChain.doFilter(request, response);
//打印返回参数
printResponseLog(response);
}
// 打印请求参数
protected HttpServletRequest printRequestLog(HttpServletRequest request) throws IOException {
StringBuilder msg = new StringBuilder();
msg.append(REQUEST_PREFIX_NAME);
// 打印request种关键信息
HttpSession session = request.getSession(false);
if (session != null) {
msg.append("sessionId = ").append(session.getId()).append("; ");
}
if (request.getMethod() != null) {
msg.append("method = ").append(request.getMethod()).append("; ");
}
if (request.getContentType() != null) {
msg.append("contentType = ").append(request.getContentType()).append("; ");
}
msg.append("uri = ").append(request.getRequestURI());
if (request.getQueryString() != null) {
msg.append('?').append(request.getQueryString());
}
// 判断是否文件上传,图片上传,视频等
if (!isMultipart(request) && !isBinaryContent(request)) {
request = new RequestWrapper(request);
msg.append("; payload = ").append(((RequestWrapper) request).getRequestBodyString(request));
}
log.info(msg.toString());
return request;
}
// 打印返回参数
private void printResponseLog(HttpServletResponse response) {
StringBuilder msg = new StringBuilder();
msg.append(RESPONSE_PREFIX_NAME);
// 判断是否请求静态资源或者文件等
if (response instanceof ResponseWrapper && !isMultipart(response) && !isBinaryContent(response)) {
ResponseWrapper responseWrapper = (ResponseWrapper) response;
msg.append("; result = ")
.append(new String(responseWrapper.toByteArray(), Charset.defaultCharset()));
}else {
Collection<String> headers = response.getHeaders(ATTACH_RESPONSE_HEADER);
if(!CollectionUtils.isEmpty(headers)){
msg.append(headers.iterator().next());
}
}
if(msg.length()> MAXLENGTH)
{
log.info(msg.substring(0, MAXLENGTH));
}else{
log.info(msg.toString());
}
}
private boolean isMultipart(HttpServletResponse response) {
return response.containsHeader(ATTACH_RESPONSE_HEADER);
}
private boolean isBinaryContent(HttpServletResponse response) {
if (response.getContentType() == null) {
return false;
}
return response.getContentType().startsWith("image")
|| response.getContentType().startsWith("video")
|| response.getContentType().startsWith("audio");
}
private boolean isBinaryContent(HttpServletRequest request) {
if (request.getContentType() == null) {
return false;
}
return request.getContentType().startsWith("image")
|| request.getContentType().startsWith("video")
|| request.getContentType().startsWith("audio");
}
private boolean isMultipart(HttpServletRequest request) {
return request.getContentType() != null
&& request.getContentType().startsWith("multipart/form-data");
}
2、将过滤器注入spring管理
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class LogAutoConfiguration {
@Bean
@ConditionalOnProperty(name = "enable-request-logging", havingValue = "true")
public LoggingFilter loggingFilter() {
return new LoggingFilter();
}
}
二、拦截器
我们的微服务框框架中,直接使用spring原生拦截器的地方还真不多,使用比较多的是三方依赖提供的拦截器,比如mybatis plus提供的分页拦截扩展,服务和服务间调用的RequestInterceptor等。找来找去,在一个独立小项目里,找到了一个拦截器的使用场景。
该项目是一个文件加解密服务,作用是三方系统将文件传递给该服务,该服务针对文件进行加解密并返回给三方系统。该服务提供了一个http的接口,三方系统调用restful接口就可以了,但是不能谁来调都能调,也是有权限控制的,这里用了一个相当简单的权限控制,给三方系统颁发一个code和密钥,拼接时间戳MD5加密来鉴权。该系统提供了加密、解密、判断密级等多个接口,不可能每个接口里加一个check吧,所以用拦截器来做最为合适。
我们常用的拦截器方法就两种,preHandle和postHandle,preHandle是controller方法执行之前,postHandle是controller方法执行之后。针对上文提到的场景,执行之前鉴权就可以了。
1、定义拦截器,并实现逻辑
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
@Component
public class PathInterceptor implements HandlerInterceptor {
@Value("${mod: 5}")
private Long mod;
@Value("${checkOpen: 1}")
private String checkOpen;
private final static Logger logger = LoggerFactory.getLogger(PathInterceptor.class);
@Autowired
private LogService logService;
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
/**
* 获取参数
*/
String appCode = request.getParameter("appCode");
String time = request.getParameter("time");
String sign = request.getParameter("sign");
MultipartFile file = null;
try {
file = ((MultipartHttpServletRequest) request).getFile("file");
}catch (Exception e)
{
logger.error("不是post文件请求");
}
String ip = IpUtil.getIpAddress(request);
/**
* 构建log
*/
SysLog sysLog = new SysLog();
sysLog.setAppCode(appCode);
if(file != null)
{
sysLog.setFileName(file.getOriginalFilename());
}
sysLog.setIp(ip);
sysLog.setFlag("0");
if("0".equals(checkOpen))
{
return true;
}
String message = "";
if(StringUtils.isEmpty(appCode) || StringUtils.isEmpty(time) || StringUtils.isEmpty(sign))
{
message = "appCode || time || sign 为空";
sysLog.setMessage(message);
logger.error(sysLog.getAppCode()+"="+message);
logService.saveLog(sysLog);
throw new BaseException(401,message);
}
String secretKey = AppService.appMap.get(appCode);
if(StringUtils.isEmpty(secretKey))
{
message = "appCode不可用";
sysLog.setMessage(message);
logger.error(sysLog.getAppCode()+"="+message);
logService.saveLog(sysLog);
throw new BaseException(401,message);
}
String signServer = DigestUtils.md5DigestAsHex((secretKey+time).getBytes());
if(!sign.equals(signServer))
{
message = "鉴权失败";
sysLog.setMessage(message);
logger.error(sysLog.getAppCode()+"="+message);
logService.saveLog(sysLog);
throw new BaseException(401,message);
}
Long timeServer = new Date().getTime();
if(Math.abs(timeServer-Long.parseLong(time))>mod*60*1000)
{
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
message = format.format(new Date())+"鉴权时间超时"+time+":"+timeServer+"mode:"+mod;
sysLog.setMessage(message);
logger.error(sysLog.getAppCode()+"="+message);
logService.saveLog(sysLog);
throw new BaseException(401,message);
}
return true;
}
}
2、将拦截器注入spring管理
这里需要定义拦截器的作用路径,我这里是匹配/**,也就是所有路径
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {
@Autowired
private PathInterceptor pathInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(pathInterceptor).addPathPatterns("/**");
}
三、切面注解
切面注解简直太强了,任何不想改原来代码的地方都可以切切切。切面注解的作用域和过滤器拦截器完全不同,不局限与web的交互层面,service层、数据连接层,还可以根据自定义注解定义切面。什么都地方都可以用,简单记录一下切面注解。
@Aspect:定义在切面实现类上的注解
@Pointcut:定义切入点
@Before:切面方法执行之前的增强
@After:切面方法执行之后的增强,不管抛异常还是正常退出都执行的增强
@AfterReturning:切面方法执行之后的增强,方法正常退出时执行
@AfterThrowing:切面方法执行之后的增强,抛出异常后执行
@Around:环绕增强,包围一个连接点的增强,可获取执行前信息,可修改执行后数据
1、demo
@Aspect
@Component
@Slf4j
public class DemoAspect {
/**
* 监控切入点
*/
@Pointcut("execution(* com.*.*Mapper.*(..))")
private void pointCutMethod() {
// 通过类路径定义切入点
}
@Pointcut("@annotation(com.es.EsInterfaceLog)")
public void annotatedMethod() {
//通过自定义注解定义切面
}
@Before("pointCutMethod()")
public void before(JoinPoint joinPoint){
System.out.println("this is before");
}
@Before("pointCutMethod(JoinPoint joinPoint)")
public void After(){
System.out.println("this is After");
}
/**
* 声明环绕通知
*
* @param pjp
* @return
* @throws Throwable
*/
@Around("pointCutMethod()")
public object doAround(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("this is aroud before");
pjp.proceed();
System.out.println("this is aroud after");
return object;
}
2、打印sql执行时间
打印sql执行时间,之前mybatisplus是有提供这个插件的,后来没了。我也预研过druid的sql执行时间,要格外的集成不说,可视化页面也是相当难用,于是就自己做了一个,并设置了开关,想开启就开启,不需要打印日志就关闭,直接上代码。
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@ConditionalOnProperty(name = "logging.mapper", havingValue = "true")
@Aspect
@Component
@Slf4j
public class MapperAspect {
@Pointcut("execution(* com.cnhtc..*.*Mapper.*(..))")
private void pointCutMethod() {
// 定义切入点
}
/**
* 声明环绕通知
*
* @param pjp
* @return
* @throws Throwable
*/
@Around("pointCutMethod()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
long begin = System.nanoTime();
Object obj = pjp.proceed();
long end = System.nanoTime();
log.debug("调用Mapper方法:{},参数:{},执行耗时:{}毫秒",
pjp.getSignature().toString(), Arrays.toString(pjp.getArgs()), (end - begin) / 1000000);
return obj;
}