Ehcache与序列化

业务老师反馈某个系统的用户添加功能突然不能用了(系统近期没有上线改动),服务器看了下日志发现是空指针问题,心想,空指针问题,那可太简单了

示意代码如下:

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class UserDTO implements Externalizable {
    private String id;
    private String username;
    private String email;
    private String role;
    private String organ;

    public UserDTO() {}

    public UserDTO(String id, String username, String email, String role, String organ) {
        this.id = id;
        this.username = username;
        this.email = email;
        this.role = role;
        this.organ = organ;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(id);
        out.writeUTF(username);
        out.writeUTF(email);
        out.writeUTF(role);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.id = in.readUTF();
        this.username = in.readUTF();
        this.email = in.readUTF();
        this.role = in.readUTF();
    }

    @Override
    public String toString() {
        return "UserDTO{" +
                "id='" + id + '\'' +
                ", username='" + username + '\'' +
                ", email='" + email + '\'' +
                ", role='" + role + '\'' +
                ", organ='" + organ + '\'' +
                '}';
    }
}


import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;

public class LoginService {
    private static final CacheManager cacheManager = CacheManager.newInstance("src/main/resources/ehcache.xml");
    private static final Cache loginUserCache = cacheManager.getCache("loginUserCache");

    // 模拟登录
    public static void login(String userId, String username, String email, String role) {
        UserDTO user = new UserDTO(userId, username, email, role);

        // 保存到 Ehcache
        loginUserCache.put(new Element(userId, user));
        System.out.println("用户已登录并写入缓存: " + user);
    }

    // 从缓存中获取用户
    public static UserDTO getUser(String userId) {
        Element element = loginUserCache.get(userId);
        if (element != null) {
            return (UserDTO) element.getObjectValue();
        }
        return null;
    }
    
}

如上代码所示,一般登陆之后,会将用户信息保存到缓存中,下次访问的时候,会从缓存中获取用户信息。某个功能发现从缓存里面取出来的user是有的, 但是机构字段却是空的,到处翻遍了代码发现,无论哪里都set了机构值,不可能为空(一整天都在到处翻代码思考到底是哪里漏set了,代码中还有一些ThreadLocal的东西,一度怀疑是不是多线程并发导致的问题)。

<!--name:缓存名称-->
    <!--maxElementsInMemory:缓存最大个数-->
    <!--eternal:缓存中对象是否为永久的,如果是,超时设置将被忽略,对象从不过期-->
    <!--timeToIdleSeconds:置对象在失效前的允许闲置时间(单位:秒),仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大-->
    <!--timeToLiveSeconds:缓存数据的生存时间(TTL),也就是一个元素从构建到消亡的最大时间间隔值,这只能在元素不是永久驻留时有效,如果该值是0就意味着元素可以停顿无穷长的时间-->
    <!--overflowToDisk:内存不足时,是否启用磁盘缓存-->
    <!--maxElementsOnDisk:设置成0 表示硬盘中最大缓存对象数无限大-->
    <!--diskPersistent:设置成true表示缓存虚拟机重启期数据磁盘存储是否在虚拟机重启后持续存在-->
    <cache name="loginUserCache"
           maxElementsInMemory="5000"
           eternal="true"
           maxElementsOnDisk="10000000"
           overflowToDisk="true"
           diskPersistent="true"
           memoryStoreEvictionPolicy="LRU">
        <!--<persistence strategy="localRestartable" synchronousWrites="false"/>-->
    </cache>

回家路上突然灵感爆发,怀疑是ehcache序列化问题,果然一查发现maxElementsInMemory配置的5000,也就是当登陆超过5000次之后, 缓存就会开始写磁盘,而USERDTO的readExternal和writeExternal方法又漏了organ字段,序列化的时候就丢失了organ字段值。最终表现出来的现象是,登陆超过5000次之后,某些需要获取用户机构的功能就失效了。