在最近的 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 数据的解析与注入。