SSH练手项目踩坑记录:Struts2接收OkHttp发送的JSON数据报空指针异常

SSH练手项目踩坑记录:Struts2接收OkHttp发送的JSON数据报空指针异常

_

在最近的 SSH(Struts2 + Spring + Hibernate)练手项目中,我尝试使用 OkHttp 向后端的 Struts2 Action 发送 JSON 格式的 POST 请求,结果遭遇了经典的 NullPointerException。这篇文章记录了完整的排查过程和最终的解决方案,算是一个非常典型的入门踩坑案例。

💥 问题描述

前端(Java 客户端)使用 OkHttp 发起 POST 请求,请求体格式为 application/json

{"password":"123456","username":"admin"}

后端 LoginAction 试图在业务逻辑中读取参数:

if ("admin".equals(loginDto.getUsername()) && "123456".equals(loginDto.getPassword())) {
    return "success";
}

运行后,程序直接在 if 这一行抛出 java.lang.NullPointerException

🔍 原因排查

经过逐步打印日志和分析底层源码,发现导致空指针的原因有两层:

1. 框架默认机制不匹配

Struts2 原生的参数拦截器(ParametersInterceptor)底层依赖 Servlet 的 request.getParameter() 方法。这个方法默认只认识 application/x-www-form-urlencoded(即标准 HTML 表单)格式的数据。当 OkHttp 发送 JSON 流时,Struts2 根本提取不到任何参数,导致属性未被赋值。

2. JSON 数据层级与后端对象不匹配

为了让 Struts2 能接收 JSON,我们引入了 Jackson 解析。但前端发送的是扁平结构的 JSON,而后端接收参数的是一个自定义对象 LoginDto loginDto。Jackson 在 Action 中找不到与 JSON 键对应的直接属性,于是默默跳过,导致 loginDto 依然为 null

🛠️ 解决方案

要彻底解决这个问题,并在不改变前端 JSON 格式的前提下优雅地接收数据,我们需要通过自定义拦截器 + ModelDriven 接口来实现。

步骤一:引入 Jackson 依赖

首先确保项目中包含了 Jackson 相关的依赖,用于 JSON 的反序列化。

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.15.2</version> <!-- 根据实际情况选择版本 -->
</dependency>

步骤二:编写自定义 JSON 拦截器

编写一个拦截器,专门拦截 application/json 请求,并利用 Jackson 动态地将 JSON 数据直接注入到 Action 或其 Model 中。

package cn.okcl.sshspringdemo.interceptor;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
import org.apache.struts2.ServletActionContext;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;

public class JsonBodyInterceptor extends AbstractInterceptor {

    private static final ObjectMapper mapper = new ObjectMapper();
    static {
        // 忽略未知属性,防止报错
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    @Override
    public String intercept(ActionInvocation invocation) throws Exception {
        HttpServletRequest request = ServletActionContext.getRequest();
        String contentType = request.getContentType();

        // 只处理 JSON 请求
        if (contentType != null && contentType.toLowerCase().contains("application/json")) {
            StringBuilder sb = new StringBuilder();
            try (BufferedReader reader = request.getReader()) {
                String line;
                while ((line = reader.readLine()) != null) {
                    sb.append(line);
                }
            }
            String jsonBody = sb.toString();

            if (jsonBody != null && !jsonBody.trim().isEmpty()) {
                Object target = invocation.getAction();
                
                // 核心:兼容 ModelDriven 模式
                if (target instanceof com.opensymphony.xwork2.ModelDriven) {
                    target = ((com.opensymphony.xwork2.ModelDriven) target).getModel();
                }
                
                // 将 JSON 属性直接注入到目标对象中
                mapper.readerForUpdating(target).readValue(jsonBody);
            }
        }
        return invocation.invoke();
    }
}

步骤三:在 struts.xml 中配置拦截器栈

我们需要将自定义的拦截器放到 Struts2 的默认拦截器栈之前,推荐在全局基础包中配置,让所有业务模块继承。

<struts>
    <!-- 定义全局基础包 -->
    <package name="base-package" namespace="/" extends="struts-default">
        <interceptors>
            <interceptor name="jsonBodyParser" class="cn.okcl.sshspringdemo.interceptor.JsonBodyInterceptor"/>
            
            <interceptor-stack name="myCustomStack">
                <!-- 自定义拦截器放在最前面 -->
                <interceptor-ref name="jsonBodyParser"/>
                <interceptor-ref name="defaultStack"/>
            </interceptor-stack>
        </interceptors>
        <!-- 设为默认拦截器栈 -->
        <default-interceptor-ref name="myCustomStack"/>
    </package>

    <!-- 业务模块引入 -->
    <include file="struts/struts-*.xml"/>
</struts>

注意:子模块(如 struts-auth.xml)中的 package 必须声明 extends="base-package" 才能生效。

步骤四:Action 实现 ModelDriven 接口

最后一步也是最关键的一步,Action 必须实现 ModelDriven 接口,并手动实例化接收对象。这样拦截器就能拿到非空的实例,并将扁平的 JSON 数据准确填充进去。

package cn.okcl.sshspringdemo.actions;

import cn.okcl.sshspringdemo.model.LoginDto;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ModelDriven;
import lombok.Data;

@Data
public class LoginAction implements ModelDriven<LoginDto> {
    
    // 必须手动 new 出来,避免空指针
    private LoginDto loginDto = new LoginDto();

    @Override
    public LoginDto getModel() {
        return loginDto;
    }

    public String print() {
        ActionContext context = ActionContext.getContext();
        context.put("city", "北京");
        
        // JSON 数据已被成功注入,此处不再报空指针
        if ("admin".equals(loginDto.getUsername()) && "123456".equals(loginDto.getPassword())) {
            return "success";
        }
        return "fail";
    }
}

📝 总结

在老牌框架(如 Struts2)与现代 HTTP 客户端(默认推崇 JSON 交互)结合的项目中,数据格式的不匹配往往是坑的源头。通过理解 Struts2 底层的 ParametersInterceptor 机制,配合 Jackson 与拦截器模式,我们可以在不侵入原本业务代码的情况下,优雅地实现 JSON 数据的解析与注入。

实战复盘:解决老 SSH 项目的循环依赖与 AOP 代理冲突 2026-07-22
Spring Boot 踩坑记录:为什么 application.yml 中的 Jackson 日期格式化对 LocalDateTime 不生效? 2026-08-14

评论区