一、Tomcat里的管道阀门(Valve)到底是啥?

1.1 生活化类比:把Tomcat比作小区外卖配送系统

你可以把Tomcat处理请求的流程类比成小区的外卖配送:骑手送达后,要经过小区保安的身份校验、快递柜的管理员登记、最后才送到你手里,每个环节都是一个“节点”,不会直接从骑手到用户。Tomcat的核心处理逻辑也是如此,整个请求处理链路是一条“管道(Pipeline)”,每个中间节点就是“阀门(Valve)”,请求会按顺序穿过每个阀门,最后才会到达你写的业务代码(比如Servlet)。这个管道是Tomcat原生自带的,不用你自己搭建,只要往链条里加自定义阀门就行。

1.2 为啥要自己写自定义Valve?

默认的Tomcat访问日志只记录IP、请求路径、响应码这类基础信息,但业务系统往往需要额外的字段,比如订单ID、用户ID,要是改业务代码太麻烦,就可以用自定义Valve,不用改动任何业务代码,直接在管道里加一个“记录自定义字段”的阀门,就能满足需求,相当于不用改变外卖流程,只加一个“核对订单号”的节点,非常灵活。

二、写个自定义的访问日志Valve,分分钟搞定

2.1 准备工作:搞清楚需要的东西

要做自定义日志阀门,只需要两样:Tomcat的API依赖(Tomcat自带,不用额外下载)、一个Java类继承Tomcat的ValveBase(基础阀门类)。最后把打好的jar包放到Tomcat的lib目录,Tomcat就能加载这个类生效,不用重启Tomcat之外的应用。

2.2 代码示例:带自定义字段的访问日志Valve

这里的技术栈是Java,完整代码带详细注释:

import org.apache.catalina.ValveBase;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.logging.Logger;

// 自定义日志阀门,继承Tomcat的基础阀门类ValveBase,符合Tomcat的插件规范
public class CustomAccessLogValve extends ValveBase {
    // 用日志记录器输出,实际项目可替换为SLF4J/Log4j,更灵活
    private static final Logger log = Logger.getLogger(CustomAccessLogValve.class.getName());

    // 核心处理方法,每个请求都会触发这个方法
    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {
        try {
            // 从请求里拿自定义字段:这里拿请求头里的用户ID、订单ID,实际可从会话、请求参数取
            String userId = request.getHeader("X-User-Id");
            String orderId = request.getHeader("X-Order-Id");
            
            // 必须调用下一个阀门,不然请求会卡在这,业务代码拿不到请求,返回500错误
            getNext().invoke(request, response);
            
            // 自定义日志格式,加入业务需要的字段,比如时间、IP、路径、用户ID、订单ID
            String customLog = String.format(
                "CUSTOM_ACCESS|%s|%s|%s|%s|%s",
                new java.util.Date(),       // 请求到达时间
                request.getRemoteAddr(),     // 客户端IP
                request.getRequestURI(),     // 请求路径
                userId != null ? userId : "Anonymous", // 用户ID,为空时默认值
                orderId != null ? orderId : "NoOrder"   // 订单ID,为空时默认值
            );
            
            // 输出日志,测试时用控制台即可
            log.info(customLog);
        } catch (Exception e) {
            // 捕获异常,避免自定义阀门的bug影响整个Tomcat运行
            log.severe("CustomValve Error: " + e.getMessage());
            // 出异常时也要继续传递请求
            getNext().invoke(request, response);
        }
    }
}

2.3 把阀门加到Tomcat,两步搞定

第一步:把上面的代码打成jar包,命名比如custom-valve-1.0.jar,放到Tomcat的lib目录(比如apache-tomcat-9.0.65/lib),这样Tomcat启动时就能加载这个类。 第二步:修改Tomcat的配置文件conf/server.xml,找到要加阀门的位置:如果要给所有虚拟主机加,就放到<Engine>标签里;如果只给单个虚拟主机加,就放到对应<Host>标签里,添加一行配置:

<!-- 自定义阀门的配置,className要换成你自己的类的全路径,比如com.example.CustomAccessLogValve -->
<Valve className="com.example.CustomAccessLogValve" />

保存后重启Tomcat,访问任意接口,就能在日志里看到自定义的字段了。

三、实现请求劫持?其实是请求的预处理和修改

3.1 啥是请求劫持?不是真的劫持请求,而是阀门里对请求做预处理

你可以理解为“提前拦截请求处理一下,再送给业务代码”,比如要统一把所有用户名转成大写,或者隐藏请求里的密码参数,都可以用这个功能,这就是题目里“请求劫持”的实际落地方式,本质是Valve对请求的修改和预处理。

3.2 代码示例:修改请求参数的自定义阀门

这里用到Tomcat提供的HttpServletRequestWrapper(请求包装类),用来修改请求内容,完整代码带注释:

import org.apache.catalina.ValveBase;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

// 自定义请求处理阀门,用来修改请求参数
public class RequestPreprocessValve extends ValveBase {
    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {
        // 把Tomcat的Request转成HTTP请求类型,方便用包装类
        HttpServletRequest httpReq = (HttpServletRequest) request;
        // 包装请求,修改参数
        HttpServletRequest wrappedRequest = new CustomRequestWrapper(httpReq);
        // 把包装后的请求传给下一个阀门,业务代码拿到的就是修改后的请求
        getNext().invoke((Request) wrappedRequest, response);
    }

    // 静态内部类:自定义请求包装类,用来修改参数,继承Tomcat的HttpServletRequestWrapper
    private static class CustomRequestWrapper extends HttpServletRequestWrapper {
        // 存储修改后的参数,用Map实现
        private final Map<String, String[]> modifiedParams;

        public CustomRequestWrapper(HttpServletRequest request) {
            super(request);
            // 复制原来的参数,避免修改原请求
            modifiedParams = new HashMap<>(request.getParameterMap());
            
            // 把username参数转成大写,比如前端传tomcat,业务代码拿到TOMCAT
            String username = request.getParameter("username");
            if (username != null) {
                modifiedParams.put("username", new String[]{username.toUpperCase()});
            }
        }

        // 重写getParameter方法,返回修改后的参数
        @Override
        public String getParameter(String name) {
            String[] values = modifiedParams.get(name);
            return (values != null && values.length > 0) ? values[0] : null;
        }

        // 重写getParameterMap方法,返回修改后的参数集合
        @Override
        public Map<String, String[]> getParameterMap() {
            return modifiedParams;
        }

        // 重写getParameterValues方法,返回修改后的参数数组
        @Override
        public String[] getParameterValues(String name) {
            return modifiedParams.get(name);
        }

        // 重写getParameterNames方法,避免框架拿不到参数名
        @Override
        public Enumeration<String> getParameterNames() {
            return java.util.Collections.enumeration(modifiedParams.keySet());
        }
    }
}

3.3 测试请求预处理的效果

你用Postman发一个请求,带username=test参数,业务代码拿到的username会变成TEST,全程业务代码不需要做任何改动,这就是请求预处理的魔力,适合统一处理参数格式、隐藏敏感参数等需求。

四、这个技术的实际应用、优缺点和注意事项

4.1 应用场景

第一个场景是自定义访问日志:比如加入订单ID、用户ID、请求耗时(在Valve里计算从请求到业务处理的时间);第二个场景是请求过滤:在Valve里检查IP是否在黑名单,是就直接返回403,不用到业务代码;第三个场景是请求统一修改:把敏感参数(比如password)替换成***,统一参数编码;第四个场景是埋点:统计接口调用次数、响应时间,不用改业务代码。

4.2 技术优缺点

优点:第一,全局生效,不用改业务代码,适合框架层面的需求;第二,性能优秀,Valve是Tomcat原生的处理逻辑,比过滤器优先级更高;第三,灵活,可配置在Engine/Host/Context层级,对应全局/虚拟主机/单个应用;缺点:第一,要懂Tomcat的Pipeline和Valve的执行顺序,放错位置会失效;第二,Valve是单例的,成员变量不能存请求相关的非线程安全数据;第三,类加载问题,多应用共用Tomcat时要注意jar包的位置。

4.3 注意事项

第一,必须调用getNext().invoke(request, response),不然请求会卡住返回500;第二,异常处理要做好,避免Valve的bug影响整个Tomcat;第三,线程安全:Valve是单例,不能在成员变量里存请求相关的变量,必须从request里取;第四,类路径:第三方依赖要放到Tomcat的lib目录,不要混在应用的jar里,避免冲突;第五,阀门顺序:先做IP过滤,再做日志,不要反过来,不然黑名单请求也会被记日志。

五、总结

Tomcat的Valve管道机制是扩展Tomcat能力的核心方式,自定义Valve既能满足自定义访问日志的需求,又能实现请求的预处理,全程不需要改动业务代码,非常适合做框架层面的功能。实战中很多公司的安全拦截、日志埋点都用这个技术,是后端开发者必须掌握的Tomcat进阶技能。