redis緩存忽略字段不匹配,並記錄日誌

發佈項目時發生了很多次因爲字段更新導致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&lt;?&gt; 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);
    }
}

測試後可行

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章