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);
    }
}

测试后可行

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