author:陈镇坤27
创建时间:2022年1月23日
创作不易,转载请注明来源
摘要:利用AOP切面、ThreadLocal、自定义注解、Spring统一异常处理等知识,自定义上传文件大小限制。
——————————————————————————————
产品需求为做一个资源上传拓展。其中涉及到限制上传视频的视频时长20分钟。我在看旧的代码时,发现旧代码是全局适配Multipart文件的一次性上传文件总和大小,并且没有抛出的合理的业务异常。所以就做了一次较大的修改。
希望自定义注解,在想要适配限制大小的接口上贴注解,输入限制文件大小值,然后系统自动会捕捉对应的文件大小,并设置判断是否拦截。
AOP切面、ThreadLocal、自定义异常处理器
(先看操作,再看解释)
首先,设计自定义注解和aop切面
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
//@Inherited //允许被子类继承(实现类本身不加此注解,也可以继承)
public @interface MultipartConfigAnnotation {
// 0:根据配置文件拦截 -1:无限制 其他 单位:byte
long sizeMax() default 0;
}
@Aspect/*aspect class*/
@Component
@Slf4j
public class MultipartAspect {
/**
* 记录调用耗时的本地Map变量
*/
private static final ThreadLocal<Long> MAX_SIZE_CONFIG_LOCAL = new ThreadLocal<>();
public static Long getMaxSizeConfigThreadLocalL(){
return MAX_SIZE_CONFIG_LOCAL.get();
}
/*切面只会切外面第一层*/
// @Pointcut("execution(* com.aopuse.jkun.aopuse.controller..*.*(..))")
@Pointcut("@annotation(com.aopuse.jkun.annotation.MultipartConfigAnnotation)")
public void LogAspect(){}
@Around("@annotation(log)")
public Object deAround(ProceedingJoinPoint joinPoint, MultipartConfigAnnotation log) throws Throwable{
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
MultipartConfigAnnotation annotation = method.getAnnotation(MultipartConfigAnnotation.class);
MAX_SIZE_CONFIG_LOCAL.set(annotation.sizeMax());
Object proceed = joinPoint.proceed();
MAX_SIZE_CONFIG_LOCAL.remove();
return proceed ;
}
}
再自定义一个文件上传处理器,并覆写父类的方法,自定义文件大小的拦截限制。
/**
* @describe:
* 注解拦截,注解自定义拦截时间
* 1、覆盖父类方法,并重写prepareFileUpload方法,加参数request
* 2、在新的prepareFileUpload方法中,获取请求路径,或特殊参数
* 3、根据特殊参数对上传文件最大值进行重复赋值
* @author: jkun(练习时长两年半)
* @return:
*/
public class EmodorMultipartResolver extends CommonsMultipartResolver {
protected CommonsFileUploadSupport.MultipartParsingResult parseRequest(HttpServletRequest request)throws MultipartException {
String encoding = determineEncoding(request);
FileUpload fileUpload = this.prepareFileUpload(encoding,request);
try {
List fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
return parseFileItems(fileItems, encoding);
} catch (FileUploadBase.SizeLimitExceededException ex) {
throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(),
ex);
} catch (FileUploadException ex) {
throw new MultipartException(
"Could not parse multipart servlet request", ex);
}
}
protected FileUpload prepareFileUpload(String encoding,HttpServletRequest request) {
FileUpload fileUpload = getFileUpload();
FileUpload actualFileUpload = fileUpload;
// Use new temporary FileUpload instance if the request specifies
// its own encoding that does not match the default encoding.
if (encoding != null && !encoding.equals(fileUpload.getHeaderEncoding())) {
actualFileUpload = newFileUpload(getFileItemFactory());
actualFileUpload.setHeaderEncoding(encoding);
// 获取ThreadLocal中的配置
Long maxSizeConfigThreadLocalL = MultipartAspect.getMaxSizeConfigThreadLocalL();
if(maxSizeConfigThreadLocalL != null) {
actualFileUpload.setSizeMax(maxSizeConfigThreadLocalL);
} else {
actualFileUpload.setSizeMax(fileUpload.getSizeMax());
}
// // 下面是根据请求路径配置
// MultipartHttpServletRequest multipartRequest =
// WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);
// boolean isAddProduct = request.getRequestURI().contains("/api/file/uploadEducationResource");
// if(isAddProduct){
// actualFileUpload.setSizeMax(300 * 1024 * 1024);//重新设置文件无大小限制
// }else{
// actualFileUpload.setSizeMax(fileUpload.getSizeMax());
// }
}
return actualFileUpload;
}
}
设置大小拦截方式有两种:
1、通过加载路径的形式;
2、通过注解的方式;
我这里选择第二种。在特定的接口上添加注解,然后切面拦截到该注解的请求,获取注解中的值。如下:
创建自定义的文件上传配置后,需要设置其为懒加载。
之后,再接口中,需要使用到下面这行代码
最后,在统一异常处理器中,增加一个异常处理方法
@ResponseStatus(value=HttpStatus.ACCEPTED)
@ExceptionHandler(MaxUploadSizeExceededException.class)
@ResponseBody
public ResponseDTO<String> handleException(MaxUploadSizeExceededException orginalException,HttpServletRequest request,HttpServletResponse response) {
String msg = "该文件大小不得大于" +
String.valueOf(orginalException.getMaxUploadSize() / (1024 * 1024)) + "M";
logger.info("[抛出文件大小异常 :" + msg + "]");
return new ResponseDTO<String>(10002, msg, null);
}
切面环切,主要作用是在进入目标方法之前,执行自定义函数。
ThreadLocal的主要作用是“跨函数传递”数据,目的是将解析到的注解值在Thread内传递。
为了方便一个线程内hreadLocalMap的调动,不反复实例该变量所在的类,我们使用static final修饰ThreadLocal变量,让Class对象持有该变量引用的实例即可。(一则方便调用,二则节省实例空间。)
因为修饰了local变量为static,该变量为类对象所持有,而类对象存在于方法区中,几乎不会被回收,所以该threadLocal实例便被一个static变量所强引用,该ThreadLocal实例便不会被回收,则同样意味ThreadLocalMap的key(该key即为ThreadLocal实例)不会被回收,则其他线程在调用ThreadLocal的get、set、remove方法时,这些key不为空的ThreadLocalMap键值对(即使不需要被使用了)无法被清除。
此外,由于线程池的作用,一个线程执行过一次ThreadLocal,该线程不消亡,加上实例为强引用,所以ThreadMap中存储的k,v无法被清除,一方面发生内存泄露,一方面会影响到线程池中该线程的复用(数据紊乱)。
所以在每次环形切面之后,都要调用remove方法将其释放。
Multipart对文件的拦截,如果不加request.getFileNames()这行代码在拦截的目标方法内,则该拦截将会在aop切面之上。
通俗地来讲,如果不加request.getFileNames(),该拦截会在请求进入controller层之前进行。
加上之后,还需要在配置中定义为懒加载,拦截会在进入请求之后执行,详细过程,可自行调试源码。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
// 检查Multi配置,其中根据父类的CommonsMultipartResolver是否懒加载决定是否优先解析MultipartResolver的文件拦截设置,细节方法如下注释
// return this.multipartResolver.resolveMultipart(request);
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null || mappedHandler.getHandler() == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (logger.isDebugEnabled()) {
logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
// 代理调用目标方法,在这里,就是我们贴注解的方法,如果上面检查是为懒处理,则将会执行到这里,成功进入controller
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
......
}
执行到controller,解析request的getFileNames()方法
@Override
public Iterator<String> getFileNames() {
return getMultipartFiles().keySet().iterator();
}
会执行getMultipartFiles方法,该方法又会去执行initializeMultipart方法。这个方法也是懒加载出的方法,见下图:
(如果懒处理为false,则立刻解析,为true,则稍后再解析)
到这里,终于会去加载我们自定义的拦截大小配置。
@ExceptionHandler注解是spring容器统一异常控制注解
一般,一个异常只会被一个处理器捕捉一次。如果再抛出异常,则不会再被捕捉。
PS:要控制由哪个处理器优先执行,是无法通过注解@order进行的。
<p><b>NOTE</b>: Annotation-based ordering is supported for specific kinds
of components only — for example, for annotation-based AspectJ
aspects. Ordering strategies within the Spring container, on the other
hand, are typically based on the {@link Ordered} interface in order to
allow for programmatically configurable ordering of each <i>instance</i>.
总而言之,order更多地用于控制下面几类(参考博客:https:///qianshangding0708/article/details/107373538)
@Aspect
标注的类ApplicationListener
实现类的加载顺序CommandLineRunner
实现类的加载顺序真正决定由哪个异常处理器处理对应的异常,根据匹配深度来进行的(参考博客:https:///weixin_34210740/article/details/93182306)