您的当前位置:首页正文

Multipart自定义资源限制文件大小限制设计——aop切面怎么才能切入Multipart的文件大小拦截?

2024-11-28 来源:个人技术集锦

Multipart自定义资源限制文件大小限制设计——aop切面怎么才能切入Multipart的文件大小拦截?

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的意义和注意事项

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方法将其释放。

在方法中使用request.getFileNames()、配置中增加懒加载标识的意义

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 &mdash; 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)

  • 控制AOP的类的加载顺序,也就是被@Aspect标注的类
  • 控制ApplicationListener实现类的加载顺序
  • 控制CommandLineRunner实现类的加载顺序

真正决定由哪个异常处理器处理对应的异常,根据匹配深度来进行的(参考博客:https:///weixin_34210740/article/details/93182306)

显示全文