发布项目时发生了很多次因为字段更新导致redis缓存字段不匹配报错,因为开发了很多的项目,为了保持所有的项目pojo类同步,我们专门搞了一个pojo项目,里面存放所有的pojo类,包括实体类和dto,放到maven上面,然后其他所有项目引用maven。
但是最近又发生了redis缓存报错的问题,原因是我们建立了项目分支系统,包括pojo类也是,然后维护人员在发布的时候可能因为没有及时切换pojo项目或者是因为编译问题,导致把分支上的pojo类发布了上去,又导致缓存报错了,虽然属于操作失误,一般来说不应该发生,但是缓存报错影响太大会导致整个系统崩溃报错。
为了避免这种情况,我们组讨论过后决定从三点下手,一是将发布的环境独立出来,专门建立独立的虚拟机,用脚本来生成war包。二是发布之前检查下pojo类的大小,看是否编译错误,三是避免字段不匹配导致的缓存报错,并且记录日志。
避免字段不匹配报错倒是简单,配置一下ObjectMapper就行
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
难的是如何记录字段不匹配,查看错误日志后发现配置是在DeserializationContext的reportUnknownProperty方法生效的
但是我又不想改源码,这样麻烦太多了,何况我想把错误记录到数据库里面。最好的办法就是重写Jackson2JsonRedisSerializer的deserialize方法,冥思苦想后我想出个绝妙的办法,那就是准备两套objectMapper,一个不忽略字段,一个忽略字段,如果不忽略字段的objectMapper报错那我就记录日志然后调用忽略字段的objectMapper。于是我模仿Jackson2JsonRedisSerializer写了自己的序列化类。
@Repository
public class MyJackson2JsonRedisSerializer implements RedisSerializer{
Logger logger = LoggerFactory.getLogger(this.getClass());
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private final JavaType javaType;
private ObjectMapper objectMapper = new ObjectMapper();//不忽略字段匹配的转化器
private ObjectMapper objectMapper2 = new ObjectMapper();//忽略字段匹配的转化器
{
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
objectMapper2.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper2.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
objectMapper2.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
public MyJackson2JsonRedisSerializer() {//为了能够被注入需要一个无参构造器
this.javaType = getJavaType(Object.class);
// TODO Auto-generated constructor stub
}
//记录序列化错误日志
@Resource
SysErrorLogDao sysErrorLogDao;
static final byte[] EMPTY_ARRAY = new byte[0];
/**
* Creates a new {@link MyJackson2JsonRedisSerializer} for the given target {@link Class}.
*
* @param type
*/
public MyJackson2JsonRedisSerializer(Class type) {
this.javaType = getJavaType(type);
}
/**
* Creates a new {@link MyJackson2JsonRedisSerializer} for the given target {@link JavaType}.
*
* @param javaType
*/
public MyJackson2JsonRedisSerializer(JavaType javaType) {
this.javaType = javaType;
}
String rex = "\\(class (?.*?)\\),";
Pattern pattern = Pattern.compile(rex);
@SuppressWarnings("unchecked")
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length == 0) {
return null;
}
try {
//使用不忽略字段匹配的转化器
return (T) this.objectMapper.readValue(bytes, 0, bytes.length, javaType);
} catch (Exception ex) {
try {
logger.error(ex.getMessage(), ex);
//报错后使用忽略字段匹配的转化器
T t = (T) this.objectMapper2.readValue(bytes, 0, bytes.length, javaType);
String content;
if(ex.getMessage().length() > 500) {
content = ex.getMessage().substring(0, 499);
}else {
content = ex.getMessage();
}
Matcher matcher = pattern.matcher(content);
String className = "";
if(matcher.find()) {//通过正则判断获得不匹配的pojo类
className = matcher.group("className");
}
SysErrorLogModel last = sysErrorLogDao.findTopByClassNameOrderByCreateDateDesc(className);
//判断最近的同类日志,如果没有或者超过十分钟了则记录一条记录
//字段不匹配后会持续报错,避免生成太多的日志
if(last == null || new Date().getTime() - last.getCreateDate().getTime() > 1000 * 60 * 10) {
SysErrorLogModel errorLog = new SysErrorLogModel();
errorLog.setContent(content);
errorLog.setType("redisDeserialize");
errorLog.setCreateDate(new Date());
errorLog.setClassName(className);
sysErrorLogDao.save(errorLog);
}
return t;
} catch (Exception e) {
// TODO Auto-generated catch block
throw new SerializationException("Could not read JSON: " + e.getMessage(), e);
}
}
}
public byte[] serialize(Object t) throws SerializationException {
if (t == null) {
return EMPTY_ARRAY;
}
try {
return this.objectMapper.writeValueAsBytes(t);
} catch (Exception ex) {
throw new SerializationException("Could not write JSON: " + ex.getMessage(), ex);
}
}
/**
* Sets the {@code ObjectMapper} for this view. If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper}
* is used.
*
* Setting a custom-configured {@code ObjectMapper} is one way to take further control of the JSON serialization
* process. For example, an extended {@link SerializerFactory} can be configured that provides custom serializers for
* specific types. The other option for refining the serialization process is to use Jackson's provided annotations on
* the types to be serialized, in which case a custom-configured ObjectMapper is unnecessary.
*/
public void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "'objectMapper' must not be null");
this.objectMapper = objectMapper;
}
public void setObjectMapper2(ObjectMapper objectMapper2) {
Assert.notNull(objectMapper, "'objectMapper' must not be null");
this.objectMapper2 = objectMapper2;
}
/**
* Returns the Jackson {@link JavaType} for the specific class.
*
* Default implementation returns {@link TypeFactory#constructType(java.lang.reflect.Type)}, but this can be
* overridden in subclasses, to allow for custom generic collection handling. For instance:
*
*
* protected JavaType getJavaType(Class<?> clazz) {
* if (List.class.isAssignableFrom(clazz)) {
* return TypeFactory.defaultInstance().constructCollectionType(ArrayList.class, MyBean.class);
* } else {
* return super.getJavaType(clazz);
* }
* }
*
*
* @param clazz the class to return the java type for
* @return the java type
*/
protected JavaType getJavaType(Class> clazz) {
return TypeFactory.defaultInstance().constructType(clazz);
}
}
配置redis
/**
* Redis缓存配置类
* @author szekinwin
*
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport{
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.timeout}")
private int timeout;
//缓存管理器
@Bean
public CacheManager cacheManager(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
//设置缓存过期时间
cacheManager.setDefaultExpiration(timeout);
return cacheManager;
}
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory){
StringRedisTemplate template = new StringRedisTemplate(factory);
setSerializer(template);//设置序列化工具
template.afterPropertiesSet();
return template;
}
@Resource
MyJackson2JsonRedisSerializer jackson2JsonRedisSerializer;
private void setSerializer(StringRedisTemplate template){
template.setValueSerializer(jackson2JsonRedisSerializer);
}
}
测试后可行