發佈項目時發生了很多次因爲字段更新導致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<T> implements RedisSerializer<T>{
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<T> 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 (?<className>.*?)\\),";
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.
* <p>
* 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.
* <p>
* 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:
*
* <pre class="code">
* protected JavaType getJavaType(Class<?> clazz) {
* if (List.class.isAssignableFrom(clazz)) {
* return TypeFactory.defaultInstance().constructCollectionType(ArrayList.class, MyBean.class);
* } else {
* return super.getJavaType(clazz);
* }
* }
* </pre>
*
* @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<String, String> 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);
}
}
測試後可行