能否在Tomcat接收请求体前拦截请求并限制文件上传大小?
当然可以在Tomcat中于请求体上传完成前执行拦截逻辑!你遇到的延迟问题,核心原因是Spring的StandardMultipartHttpServletRequest是在Tomcat已经把整个请求体完整接收并解析后才触发检查的,所以会先等大文件传完才给你反馈。要实现像Nginx那样即时中断的效果,得从Tomcat的底层配置或者扩展机制入手,下面给你两个靠谱的方案:
一、直接配置Tomcat Connector参数(最简单)
Tomcat的Connector本身就自带了请求体大小限制的配置,而且是在底层接收请求时就生效,完全不用等应用层逻辑执行:
设置
maxPostSize:这个参数控制POST请求的最大体大小,单位是字节。比如要限制10MB,就设置成10485760(1010241024)。注意Tomcat 8.5及以后版本中,maxPostSize默认值是-1(不限制),之前的版本默认是2MB。
如果你用Spring Boot,可以在application.properties里这样配置:server.tomcat.max-post-size=10485760要是直接改Tomcat的
server.xml,就在Connector标签里加:<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" maxPostSize="10485760" />配合
maxSwallowSize优化体验:这个参数控制当服务器拒绝请求后,是否继续接收剩余的请求体。设置为0的话,Tomcat会立即断开连接,避免继续浪费带宽接收大文件。Spring Boot里可以这样配置:server.tomcat.max-swallow-size=0
不过这个方案有个小局限:如果请求用了chunked编码(没有Content-Length头),maxPostSize就不起作用了,这时候得用下面的自定义Valve方案。
二、自定义Tomcat Valve(更灵活,支持chunked编码)
Valve是Tomcat请求处理链条中非常靠前的组件,比Servlet过滤器还要早执行,能在请求体接收过程中实时监控字节数,一旦超过限制就直接中断请求,完美解决chunked编码的场景:
自定义Valve类:实现
org.apache.catalina.Valve接口,或者继承org.apache.catalina.valves.ValveBase,重写invoke方法,在里面监控请求的输入流:public class FileSizeLimitValve extends ValveBase { private long maxSize = 10 * 1024 * 1024; // 10MB @Override public void invoke(Request request, Response response) throws IOException, ServletException { // 只拦截multipart类型的请求 String contentType = request.getContentType(); if (contentType != null && contentType.startsWith("multipart/")) { request.setInputStream(new SizeLimitedInputStream(request.getInputStream(), maxSize, response)); } getNext().invoke(request, response); } // 内部类:包装输入流,实时监控读取的字节数 private static class SizeLimitedInputStream extends ServletInputStream { private final ServletInputStream delegate; private final long maxSize; private final Response response; private long totalRead = 0; public SizeLimitedInputStream(ServletInputStream delegate, long maxSize, Response response) { this.delegate = delegate; this.maxSize = maxSize; this.response = response; } @Override public int read() throws IOException { int read = delegate.read(); if (read != -1) { totalRead++; checkSize(); } return read; } @Override public int read(byte[] b, int off, int len) throws IOException { int read = delegate.read(b, off, len); if (read != -1) { totalRead += read; checkSize(); } return read; } private void checkSize() throws IOException { if (totalRead > maxSize) { response.setStatus(HttpStatus.PAYLOAD_TOO_LARGE.value()); response.getWriter().write("File size exceeds limit of " + maxSize / 1024 / 1024 + "MB"); // 关闭输入流,中断请求 delegate.close(); throw new IOException("File size exceeds limit"); } } // 实现其他ServletInputStream的抽象方法,直接委托给delegate @Override public boolean isFinished() { return delegate.isFinished(); } @Override public boolean isReady() { return delegate.isReady(); } @Override public void setReadListener(ReadListener listener) { delegate.setReadListener(listener); } } // setter方法,方便配置maxSize public void setMaxSize(long maxSize) { this.maxSize = maxSize; } }注册Valve到Tomcat:如果是Spring Boot项目,可以通过
TomcatServletWebServerFactory来添加自定义Valve:@Configuration public class TomcatConfig { @Bean public TomcatServletWebServerFactory tomcatServletWebServerFactory() { TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); factory.addEngineValves(new FileSizeLimitValve()); return factory; } }
这个方案不管是有Content-Length的请求还是chunked编码的请求,都能实时监控,一旦超过大小就立即中断,和Nginx的效果完全一致。
总结
- 如果你只需要处理普通的非chunked请求,直接配置Tomcat的
maxPostSize和maxSwallowSize是最简单的方案,零代码改动。 - 如果需要支持chunked编码的请求,或者需要更灵活的拦截逻辑(比如根据不同路径设置不同大小限制),自定义Valve是最优解。
这两种方案都能在Tomcat底层接收请求体的过程中就执行拦截,完全不会出现你之前碰到的等待大文件上传完成的延迟问题。
内容的提问来源于stack exchange,提问作者eugene82

