transient关键字:序列化时哪些字段不应该被持久化
transient关键字:序列化时哪些字段不应该被持久化
适读人群:Java初中级开发者、做过序列化/RPC/缓存开发的后端工程师 | 阅读时长:约12分钟 | 文章类型:知识点深挖+实战场景
开篇故事
几年前,有个同事接手了一个老系统的维护工作。这个系统把用户Session对象直接序列化存到Redis里。
某天他排查一个奇怪的问题:用户登录后,从Redis取出Session,但里面的数据库连接对象(Connection字段)总是null。他以为是代码bug,排查了半天,最后才发现那个Connection字段标记了transient:
public class UserSession implements Serializable {
private String userId;
private String token;
private transient Connection dbConnection; // 这个字段不会被序列化
}他不知道transient是什么,把它删掉了,以为这样就好了……
结果序列化一个数据库连接到Redis,然后反序列化的时候,拿到的是一个已经关闭的或者完全无效的连接。系统报了一大堆连接相关的错误。
这件事让他学到了两件事:不认识的关键字,先搞清楚它的用途,别随手删。今天把transient说清楚。
一、transient关键字是什么
transient是Java的一个修饰符,用于修饰字段。被transient修饰的字段,在对象序列化时会被跳过,不写入序列化数据流。反序列化时,这些字段会被赋予该类型的默认值(基本类型是0/false,引用类型是null)。
public class Example implements Serializable {
private String name; // 会被序列化
private transient String password; // 不会被序列化,反序列化后是null
private transient int tempValue; // 不会被序列化,反序列化后是0
}二、核心原理深挖
哪些字段应该标记transient
判断一个字段是否应该标记transient,问这些问题:
这个字段的值能通过其他字段重新计算出来吗?
- 能的话,序列化它是浪费,反序列化后重新计算即可
这个字段代表一个"活着的资源",序列化没有意义吗?
- 数据库连接、网络Socket、文件句柄——序列化了也没用,反序列化后需要重新获取
这个字段包含敏感信息,不应该持久化吗?
- 密码、秘钥、token——不应该以明文序列化到文件、缓存或网络
这个字段的类型不支持序列化(未实现Serializable)吗?
- 如果字段类型没有实现Serializable,序列化整个对象会报NotSerializableException
serialVersionUID的重要性
serialVersionUID不是transient相关的,但序列化时必须提一下。
public class UserDTO implements Serializable {
private static final long serialVersionUID = 1L; // 强烈推荐手动定义
// ...
}如果不定义,JVM会根据类的结构自动生成。一旦类的结构变化(加字段、改方法名),自动生成的serialVersionUID就变了,反序列化旧数据时会抛:
java.io.InvalidClassException: UserDTO; local class incompatible:
stream classdesc serialVersionUID = -8432658367742312345,
local class serialVersionUID = 7612039812345678901三、完整代码实现
代码一:transient的基本用法和验证
package com.laozhang.keyword.transient_;
import java.io.*;
/**
* transient关键字完整验证
*/
public class TransientDemo {
static class UserSession implements Serializable {
private static final long serialVersionUID = 1L;
private String userId;
private String username;
// 敏感信息:不序列化
private transient String rawPassword;
// 可重新获取的资源:不序列化
private transient Connection dbConnection;
// 缓存字段(可重算):不序列化
private transient String displayName; // username + userId的拼接
// 静态字段:本来就不参与序列化(不需要transient,但也不会被序列化)
private static int instanceCount = 0;
UserSession(String userId, String username, String rawPassword) {
this.userId = userId;
this.username = username;
this.rawPassword = rawPassword;
this.displayName = username + "#" + userId;
instanceCount++;
}
// 反序列化后,transient字段是null/0
// 可以实现readObject方法,在反序列化时重新初始化这些字段
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 先反序列化非transient字段
// 重新初始化可以重算的字段
this.displayName = username + "#" + userId;
// dbConnection和rawPassword需要通过其他途径获取,这里不处理
}
@Override
public String toString() {
return "UserSession{userId=" + userId + ", username=" + username +
", rawPassword=" + rawPassword + // null(被transient保护)
", displayName=" + displayName +
", dbConnection=" + (dbConnection != null ? "active" : "null") + "}";
}
}
// 模拟Connection(不支持序列化的类型)
static class Connection {
String url;
Connection(String url) { this.url = url; }
}
public static void main(String[] args) throws Exception {
UserSession session = new UserSession("user001", "张三", "secret123");
session.dbConnection = new Connection("jdbc:mysql://localhost/db");
System.out.println("序列化前: " + session);
// 序列化到字节数组
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(session);
}
byte[] bytes = bos.toByteArray();
System.out.println("序列化字节数: " + bytes.length);
// 反序列化
UserSession restored;
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
restored = (UserSession) ois.readObject();
}
System.out.println("反序列化后: " + restored);
// transient字段:rawPassword=null, dbConnection=null
// readObject重建:displayName=张三#user001
}
}代码二:实战场景下transient的正确使用
package com.laozhang.keyword.transient_;
import java.io.*;
import java.math.BigDecimal;
import java.util.*;
/**
* 实际项目中transient的常见使用场景
*/
public class TransientPractice {
/**
* 场景1:含敏感字段的用户对象
* 序列化后用于缓存/传输,不暴露密码
*/
static class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String email;
// 密码不序列化
private transient String hashedPassword;
// 认证token不序列化(token有自己的持久化方式)
private transient String accessToken;
User(Long id, String username, String email, String hashedPassword) {
this.id = id;
this.username = username;
this.email = email;
this.hashedPassword = hashedPassword;
}
public void setAccessToken(String token) { this.accessToken = token; }
@Override
public String toString() {
return String.format("User{id=%d, username='%s', password=%s, token=%s}",
id, username,
hashedPassword != null ? "****" : "null",
accessToken != null ? "****" : "null");
}
}
/**
* 场景2:带缓存计算字段的商品对象
* finalPrice是缓存的计算结果,可以重算
*/
static class Product implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private BigDecimal originalPrice;
private double discountRate;
// 计算缓存:finalPrice = originalPrice * discountRate
// 可以重算,不需要序列化
private transient BigDecimal finalPrice;
Product(String id, BigDecimal originalPrice, double discountRate) {
this.id = id;
this.originalPrice = originalPrice;
this.discountRate = discountRate;
recalculateFinalPrice();
}
private void recalculateFinalPrice() {
this.finalPrice = originalPrice.multiply(BigDecimal.valueOf(discountRate));
}
// 反序列化后重新计算
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
recalculateFinalPrice(); // 重新计算transient字段
}
public BigDecimal getFinalPrice() {
if (finalPrice == null) recalculateFinalPrice(); // 懒计算
return finalPrice;
}
@Override
public String toString() {
return String.format("Product{id=%s, original=%.2f, final=%.2f}",
id, originalPrice, getFinalPrice());
}
}
/**
* 场景3:序列化时包含不支持Serializable的字段
*/
static class OrderProcessor implements Serializable {
private static final long serialVersionUID = 1L;
private String processorId;
// Thread不支持Serializable,必须标记transient
private transient Thread workerThread;
// Logger通常不支持Serializable,标记transient
private transient org.slf4j.Logger logger;
OrderProcessor(String id) {
this.processorId = id;
initResources();
}
private void initResources() {
this.workerThread = new Thread(() -> {}, "worker-" + processorId);
this.logger = org.slf4j.LoggerFactory.getLogger(OrderProcessor.class);
}
// 反序列化后重新初始化资源
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
initResources(); // 重新获取/创建资源
}
}
public static void main(String[] args) throws Exception {
System.out.println("=== 用户对象序列化(密码保护)===");
User user = new User(1L, "张三", "zhang@example.com", "hashed_secret");
user.setAccessToken("eyJhbGci...");
System.out.println("序列化前: " + user);
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
new ObjectOutputStream(bos).writeObject(user);
// 反序列化
User restored = (User) new ObjectInputStream(
new ByteArrayInputStream(bos.toByteArray())).readObject();
System.out.println("反序列化后: " + restored); // password=null, token=null
System.out.println("\n=== 商品对象序列化(缓存字段重算)===");
Product product = new Product("P001", new BigDecimal("100.00"), 0.9);
System.out.println("序列化前: " + product + ", finalPrice=" + product.getFinalPrice());
ByteArrayOutputStream bos2 = new ByteArrayOutputStream();
new ObjectOutputStream(bos2).writeObject(product);
Product restoredProduct = (Product) new ObjectInputStream(
new ByteArrayInputStream(bos2.toByteArray())).readObject();
System.out.println("反序列化后: " + restoredProduct + ", finalPrice=" + restoredProduct.getFinalPrice());
// finalPrice通过readObject重新计算,值正确
}
}四、踩坑实录
坑1:不知道transient作用就删除,导致序列化不该序列化的字段
这就是开篇的案例。删掉transient后,把不支持序列化的Connection对象放入序列化流,得到的是无效的连接状态,系统报各种连接错误。
解法: 看到不认识的关键字,先查,不要随手删。transient字段通常是有意为之的。
坑2:transient字段反序列化后是null,没有处理空指针
报错现象:
java.lang.NullPointerException: Cannot invoke "Logger.info(String)" because "this.logger" is null
at com.example.service.OrderProcessor.process(OrderProcessor.java:35)根本原因:
logger被标记为transient(因为Logger不支持序列化),反序列化后logger是null。代码里直接使用而没有null检查。
具体解法:
// 方案1:实现readObject,反序列化后重新初始化
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
logger = LoggerFactory.getLogger(this.getClass()); // 重新获取
}
// 方案2:懒初始化(每次使用前检查)
private Logger getLogger() {
if (logger == null) {
logger = LoggerFactory.getLogger(this.getClass());
}
return logger;
}
// 方案3:把Logger定义为static(static字段不参与序列化)
private static final Logger logger = LoggerFactory.getLogger(OrderProcessor.class);坑3:序列化含transient字段的对象后,版本升级忘更新serialVersionUID
报错现象:
java.io.InvalidClassException: com.example.UserSession; local class incompatible:
stream classdesc serialVersionUID = 3713093631012345678,
local class serialVersionUID = -5678901234567890123根本原因:
没有手动定义serialVersionUID,类结构修改后(比如添加或删除transient字段),JVM重新计算的serialVersionUID变了,旧数据无法反序列化。
具体解法:
任何实现Serializable的类,都应该手动定义serialVersionUID:
public class UserSession implements Serializable {
private static final long serialVersionUID = 1L; // 手动定义,只在有意不兼容时才改
// ...
}如果类结构变化但仍然需要兼容旧数据(比如新增字段),保持serialVersionUID不变,新字段会被赋默认值。如果确实不兼容,才更新serialVersionUID(并处理旧数据迁移)。
五、总结与延伸
transient关键字的使用场景,记住四类:
1. 敏感信息:密码、秘钥、token
2. 活着的资源:连接、Socket、线程、文件句柄
3. 可重算的缓存:派生字段、统计值
4. 不支持序列化的类型:Logger、Thread、Connection等
使用transient的注意事项:
- 反序列化后transient字段是默认值(null/0/false)
- 如果需要反序列化后恢复这些字段,实现
readObject()方法 - 静态字段(
static)本来就不参与序列化,不需要加transient - final字段可以是transient,但反序列化后无法重新赋值(只有默认值)
另外,Java序列化的性能不太好,在高性能场景下,更多人选择Protobuf、Kryo等序列化框架。但transient关键字的语义在这些框架里通常也被尊重,或者有类似的注解(如Kryo的@SkipDefaultSerializer)。
