缘起

由于项目复杂起来, 导致平时部署的时候常常会有多个Tomcat实例来运行支撑.

比如我们公司里有个 DSP 项目, 分为三个大类的 Tomcat(Web 处理用户请求, Task 定时任务及监听器之类的, Bid 负责竞价), 而每个Tomcat, 常常又有负载匀衡来处理, 所以导致配置文件的属性值要修改一下的话, 常常需要修改所有 Tomcat 然后重启.

这就给我们带来非常大的麻烦, 一来容易影响业务的正常运行时间, 二来这些繁琐冗余的操作, 操作多几次, 就有点烦了, 所以想着有没有一些偷懒的方法?

处理办法

  1. 使用 Spring Cloud Config 配置中心服务来进行处理. 一来, 我们项目是比较早就开始的, 所以当时并没有用 Spring Boot . 所以, 这个方案, 可能看后面改造为 Spring boot 的时候, 或许可以考虑一下.

  2. 使用 DB , 然后每个服务定时刷新一次.

  3. 使用 RabbitMQ , 进行消息发布订阅

  4. 使用 Redis, 进行消息发布订阅

总的来看, 3, 4 的方案比较好, 一来不用定时轮询(pull), 而是订阅相应的消息即可(publish). 最终我使用了 Redis 的来处理.

示例代码

使用 Redis 的 Pub/Sub 来进行消息发布/订阅. Java 中订阅者的配置及代码:

    <bean id="messageListnerContainer" class="org.springframework.data.redis.listener.RedisMessageListenerContainer" destroy-method="destroy">
        <property name="connectionFactory" ref="jedisConnectionFactory"/>
        <property name="messageListeners">
            <map>
                <entry key-ref="redisPropertiesMessageListener">
                    <bean class="org.springframework.data.redis.listener.ChannelTopic">
                        <constructor-arg value="${redis.channel.properties}"/>
                    </bean>
                </entry>
            </map>
        </property>
    </bean>

动态更新属性的逻辑代码


/**
 * 修改属性的 redis listener <br />
 * 例子
 * <p>
 *     redis
 * PUBLISH 'channel.properties' '{"k":"max.bidding.task", "v":"15"}'
 */
@Component
public class RedisPropertiesMessageListener implements MessageListener {

    @Autowired
    @Qualifier("redisTemplate")
    private RedisTemplate redisTemplate;

    @Autowired
    private ReloadableProperties reloadableProperties;

    /**
     * 监听动态修改 properties
     *
     * @param message
     * @param bytes
     */
    @Override
    public void onMessage(Message message, byte[] bytes) {
        byte[] body = message.getBody();
        String jsonStr = (String) redisTemplate.getValueSerializer().deserialize(body);
        Loggers.RUNNING_LOG.info("new message from properties channel. json:{}", jsonStr);
        JsonNode msgJsonNode = JsonUtil.readTree(jsonStr);
        final String key = msgJsonNode.get("k").asText();
        final String value = msgJsonNode.get("v").asText();
        reloadableProperties.modify(key, value);
    }
}

修改属性的反射处理代码

    public void modify(final String modifyKey, final String value) {
        final String key = "${" + modifyKey.trim() + "}";
        Loggers.AUDIT_LOG.info("modify k=>{} value=>{}", key, value);
        Field[] fields = ReloadableProperties.class.getDeclaredFields();
        try {
            for (Field field : fields) {
                field.setAccessible(true);
                final Value fieldKey = field.getAnnotation(Value.class);
                final String fieldAnnotationValue = fieldKey.value();
                if (key.equals(fieldAnnotationValue)) {
                    //如果找到匹配的.
                    Class fieldType = field.getType();
                    if (fieldType == int.class || field.getType() == Integer.class) {
                        field.setInt(this, Integer.parseInt(value));
                    } else if (fieldType == String.class) {
                        field.set(this, value);
                    } else if (fieldType == Double.class || fieldType == double.class) {
                        field.setDouble(this, Double.parseDouble(value));
                    } else if (fieldType == Long.class || fieldType == long.class) {
                        field.setLong(this, Long.parseLong(value));
                    } else if (fieldType == Float.class || fieldType == float.class) {
                        field.setFloat(this, Float.parseFloat(value));
                    } else if (fieldType == Short.class || fieldType == short.class) {
                        field.setShort(this, Short.parseShort(value));
                    } else if (fieldType == Float.class || fieldType == float.class) {
                        field.setFloat(this, Float.parseFloat(value));
                    } else if (fieldType == Byte.class || fieldType == byte.class) {
                        field.setByte(this, Byte.parseByte(value));
                    } else if (fieldType == Boolean.class || fieldType == boolean.class) {
                        field.setBoolean(this, Boolean.parseBoolean(value));
                    } else if (fieldType == Character.class || fieldType == char.class) {
                        field.setChar(this, value.charAt(0));
                    }
                    break;
                }
            }
        } catch (IllegalAccessException e) {
            Loggers.ERROR_LOG.error(e.getMessage(), e);
        }
    }

注意, 因为这里使用了 Spring 的 @Value 注解来注入属性的, 所以这里的处理逻辑是利用该注解的值(形式类似 @Value("${xxx}") 它的值就是 ${xxx}) , 这样子就可以利用反射统一来修改同类型的属性值的.

修改

在相应的配置的 redis 上, 输入以下命令即可:

PUBLISH 'channel.properties' '{"k":"max.bidding.task", "v":"15"}'

这样子, 就会将属性的 key 为 max.bidding.task 的值, 动态设置为 15 了.

注意事项

如果只是临时这样子处理, 然后中间又重启过服务的话要特别注意

  1. 要么要重启前, 在文件中将相应的值设置为线上当前状态的一致(不然容易出 Bug, 也很难排查)
  2. 要么将刷新的 redis 命令全部重新发布一次