I want to fly higher
programming Explorer
posts - 114,comments - 263,trackbacks - 0
1.概述
   本篇随笔主要讲述了在线程序通过脚本或者代码进行更新的一个例子.
       a.在线程序通常会有更改内存数据或者修复错误逻辑的需求.
       b.更改内存数据则通常是找到要修改的对象,然后直接通过加载更新脚本更新对象数据.
       c.更改错误逻辑,通常是新写一个class,继承出错的类并覆写出错的逻辑方法,然后将所有class的实例对象重新替换一下,一定要注意数据的拷贝,即从旧对象复制到新对象.
       d.本篇通过两种方式,一种更新代码就是.java,在外部编译好.class,然后通知在线程序进行更新,在线程序利用classloader加载更新代码,然后执行更新操作(所有的脚本都会实现一个更新接口).
       e.第二种方式就是直接使用更新脚本groovy,groovy相比java来说,写起来更简单(太tm方便了),然后在线程序这边直接通过GroovyScriptEngine直接运行groovy更新脚本.
_____________________________________________________________________________________________________________________
2.例子
  本文的例子是有一个Player对象,该对象有几个属性和一个方法。更新脚本则是在线修改玩家对象数据以及修改错误的方法.

3.代码.

package com.mavsplus.example.java.compile;

/**
 * 测试的玩家对象
 * 
 * @author landon
 * @since 1.8.0_25
 
*/
public class Player implements Cloneable {

    // ---三个测试属性,需要有动态修改的需求,如线上玩家等级出错,需要脚本将内存中的值直接修改为正确的值 --//
    public long id;
    public String name;
    public int lv;
    public int vipLv;

    // -- 测试方法,需要有动态修改的需求,如线上的时候发现该方法实现有问题,需要将方法逻辑修正为正确的实现 --//
    public boolean isVip() {
        return vipLv > 0;
    }

    @Override
    public String toString() {
        return "Player [id=" + id + ", name=" + name + ", lv=" + lv + ", vipLv=" + vipLv + "]";
    }

    @Override
    public Object clone() {
        try {
            Player clonePlayer = (Player) super.clone();

            return clonePlayer;
        } catch (CloneNotSupportedException e) {
            throw new InternalError();
        }
    }
}


package com.mavsplus.example.java.compile;

/**
 * 脚本执行接口
 * 
 * @author landon
 * @since 1.8.0_25
 
*/
public interface IScriptExecute {

    public void execute();
}


package com.mavsplus.example.java.compile.script;

import com.mavsplus.example.java.compile.IScriptExecute;
import com.mavsplus.example.java.compile.OnlineServer;
import com.mavsplus.example.java.compile.Player;
import com.mavsplus.example.java.compile.PlayerService;

/**
 * 修改Player内存数据脚本
 * 
 * @author landon
 * @since 1.8.0_25
 
*/
public class ModifyPlayerFieldScript implements IScriptExecute {

    public void execute() {
        System.out.println("Execute Script:ModifyPlayerFieldScript");

        PlayerService playerService = OnlineServer.playerService;

        if (playerService != null) {
            // 注意这里是5L,如果5的话则返回null.5默认为int,而Key的参数是Long,切记
            Player errPlayer = playerService.playerMap.get(5L);

            if (errPlayer != null) {
                errPlayer.lv = 10086;
                errPlayer.vipLv = 11;
            }
        }
    }
}


package com.mavsplus.example.java.compile.script;

import java.util.ArrayList;
import java.util.List;

import com.mavsplus.example.java.compile.IScriptExecute;
import com.mavsplus.example.java.compile.OnlineServer;
import com.mavsplus.example.java.compile.Player;

/**
 * 修改Player线上业务逻辑
 * 
 * @author landon
 * @since 1.8.0_25
 
*/
public class ModifyPlayerLogicScript implements IScriptExecute {

    @Override
    public void execute() {
        System.out.println("Execute Script:ModifyPlayerLogicScript");

        List<Player> newPlayers = new ArrayList<>();

        // 这样更新的一个很大的问题在于得找到所有的Player对象,如果Player不在线怎么办?
        // 改进:线上其实可以在获取Player对象的方法中进行统一进行处理(均都根据id查询Player,用脚本覆写改方法返回ModifiedPlayer且重新加入cache).-->这样就不care是否是在线玩家还是离线玩家了
        for (Player oldPlayer : OnlineServer.playerService.playerMap.values()) {
            // 如果是多线程处理的这里也可以直接进行克隆
            ModifiedPlayer newPlayer = new ModifiedPlayer(oldPlayer);
            newPlayers.add(newPlayer);
        }

        for (Player newPlayer : newPlayers) {
            OnlineServer.playerService.playerMap.put(newPlayer.id, newPlayer);
        }
    }

    // 修改的Player对象,修正了方法逻辑实现,需要将线上的对象给替换掉,注意数据的克隆
    private static class ModifiedPlayer extends Player {

        public ModifiedPlayer(Player player) {
            this.id = player.id;
            this.name = player.name;
            this.vipLv = player.vipLv;
            this.lv = player.lv;
        }

        // 覆写错误的数据逻辑
        @Override
        public boolean isVip() {
            return false;
        }
    }
}



package com.mavsplus.example.java.compile.groovy

import com.mavsplus.example.java.compile.OnlineServer;
import com.mavsplus.example.java.compile.Player;
import com.mavsplus.example.java.compile.PlayerService;

// 同ModifyPlayerFieldScript,不过由groovy实现
// 生成:target\classes\com\mavsplus\example\java\compile\groovy\ModifyPlayerField.class
def execute() {
    println "execute groovy:ModifyPlayerField";

    PlayerService playerService = OnlineServer.playerService;

    if (playerService != null) {
        Player errPlayer = playerService.playerMap.get(7L);

        if (errPlayer != null) {
            errPlayer.lv = 99999;
            errPlayer.vipLv = 15;
        }
    }
}

execute();


package com.mavsplus.example.java.compile.groovy

import java.util.ArrayList;
import java.util.List;

import com.mavsplus.example.java.compile.OnlineServer;
import com.mavsplus.example.java.compile.Player;

// 生成:target\classes\com\mavsplus\example\java\compile\groovy\ModifiedPlayer.class    ModifyPlayerLogic.class
class ModifiedPlayer extends Player {
    ModifiedPlayer(Player player) {
        this.id = player.id;
        this.name = player.name;
        this.vipLv = player.vipLv;
        this.lv = player.lv;
    }

    // 覆写错误的数据逻辑
    @Override
    boolean isVip() {
        return vipLv > 5;
    }
}

def execute(){
    println "Execute groovy:ModifyPlayerLogic"

    List<Player> newPlayers = new ArrayList<>();

    for (Player oldPlayer : OnlineServer.playerService.playerMap.values()) {
        ModifiedPlayer newPlayer = new ModifiedPlayer(oldPlayer);
        newPlayers.add(newPlayer);
    }

    for (Player newPlayer : newPlayers) {
        OnlineServer.playerService.playerMap.put(newPlayer.id, newPlayer);
    }
}

execute();


package com.mavsplus.example.java.compile;

import groovy.util.GroovyScriptEngine;

import java.io.File;

/**
 * 
 * 脚本更新服务,用来加载script目录下脚本class
 * 
 * <p>
 * 线上更新的时候可以将脚本打成包,并将包放在classpath下
 * 
 * <p>
 * 当然也可以放到一个一个专门的目录,用自定义classloader进行加载
 * 
 * <p>
 * start0方法用来执行groovy
 * 
 * @author landon
 * @since 1.8.0_25
 
*/
public class ScriptUpdateService {

    public void loadScriptAndExecute(String scriptClassName) throws Exception {
        String fullName = "com.mavsplus.example.java.compile.script." + scriptClassName;

        Class<IScriptExecute> clazz = (Class<IScriptExecute>) ClassLoader.getSystemClassLoader().loadClass(fullName);
        IScriptExecute instance = clazz.newInstance();

        instance.execute();
    }

    public void start() {
        try {
            // 扫描script目录
            String scriptPath = getClass().getResource("").getPath() + "\\script";

            File file = new File(scriptPath);
            File[] subFiles = file.listFiles();

            for (File subFile : subFiles) {
                String fileClassName = subFile.getName();
                int dotCharIndex = fileClassName.indexOf('.');

                String loadClassName = fileClassName.substring(0, dotCharIndex);

                if (loadClassName.contains("$")) {
                    continue;
                }

                loadScriptAndExecute(loadClassName);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 加载groovy
    public void start0() {
        try {
            String groovyPath = "E:\\github\\mavsplus-all\\mavsplus-examples\\src\\main\\java\\com\\mavsplus\\example\\java\\compile\\groovy";
            GroovyScriptEngine engine = new GroovyScriptEngine(groovyPath);

            engine.run("ModifyPlayerField.groovy""");
            engine.run("ModifyPlayerLogic.groovy""");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


package com.mavsplus.example.java.compile;

import java.util.HashMap;
import java.util.Map;

/**
 * 玩家服务
 * 
 * @author landon
 * @since 1.8.0_25
 
*/
public class PlayerService {

    // -- 玩家map --//
    public Map<Long, Player> playerMap = new HashMap<>();

    public void start() {
        for (Player player : playerMap.values()) {
            System.out.println(player);
            System.out.println("isVip:" + player.isVip());
        }
    }
}


package com.mavsplus.example.java.compile;

import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

/**
 * 线上运行的服务程序
 * 
 * <p>
 * 该example的主要目的在于测试在程序运行过程中,需要改变一些内存中的一些数据或者操作,需要动态修改.目前初步的实现方式是服务程序可以接收一个.java
 * , 这个可以理解为补丁或者更新脚本,程序收到该脚本后,在内存中动态编译执行,进而修改一些数据或者操作
 * 
 * <p>
 * 当然也可以直接将补丁脚本编译好,然后将编译后的class放到classpath下,通知服务程序用classloader进行加载并更新.
 * 
 * <p>
 * 后续可以使用jvm脚本如groovy做对比分析
 * 
 * @author landon
 * @since 1.8.0_25
 
*/
public class OnlineServer {

    public static PlayerService playerService;
    public static ScriptUpdateService scriptUpdateService;

    public static void init() {
        playerService = new PlayerService();
        scriptUpdateService = new ScriptUpdateService();
    }

    public static void initData() {
        // 初始化部分测试数据

        for (int i = 0; i < 10; i++) {
            Player player = new Player();

            player.id = i + 1;
            player.lv = ThreadLocalRandom.current().nextInt(100+ 1;
            player.name = "landon" + i;
            player.vipLv = ThreadLocalRandom.current().nextInt(10+ 1;

            playerService.playerMap.put(player.id, player);
        }
    }

    // 模拟服务启动
    public static void start() throws Exception {
        while (true) {
            playerService.start();

            System.out.println("----------------------------------");

            TimeUnit.SECONDS.sleep(5);

            // 目前测试这样操作,实际线上通过网络消息通知更新服务器更新具体的某个脚本即可
            scriptUpdateService.start();
            playerService.start();

            System.out.println("----------------------------------");

            TimeUnit.SECONDS.sleep(5);

            // 加载groovy脚本
            scriptUpdateService.start0();
            playerService.start();

            System.out.println("----------------------------------");
        }
    }

    public static void main(String[] args) throws Exception {
        init();
        initData();

        start();
    }
}


4.代码部分解释
    1.OnlineServer是一个在线运行的程序,它包括一个玩家服务。目前程序只是简单打印所有的玩家信息.
    2.程序运行的时候,比如我们发现线上某一个Player的数据有问题,我们需要进行在线进行修正,所以我们会写一个更新脚本ModifyPlayerFieldScript.java,然后将其编译.然后通知在线程序利用classloader加载更新脚本并执行(执行默认的execute方法)
    3.作为对比,同时写了功能一样的groovy更新脚本,然后通知在线程序利用GroovyScriptEngine直接运行指定的groovy脚本.
    4.同时程序运行的时候,会发现Player类的一个方法实现有bug,我们需要在线进行修正,所以我们会写一个更新脚本ModifyPlayerLogicScript.java(主要原理在于继承Player并覆写出错方法),然后将其编译,然后通知在在线程序更新.
    5.作为对比,也同样写了一个功能一样的groovy更新脚本实现.


5.输出:

Player [id=1, name=landon0, lv=93, vipLv=6]
isVip:true
Player [id=2, name=landon1, lv=37, vipLv=9]
isVip:true
Player [id=3, name=landon2, lv=25, vipLv=2]
isVip:true
Player [id=4, name=landon3, lv=78, vipLv=4]
isVip:true
Player [id=5, name=landon4, lv=27, vipLv=8]
isVip:true
Player [id=6, name=landon5, lv=84, vipLv=1]
isVip:true
Player [id=7, name=landon6, lv=95, vipLv=4]
isVip:true
Player [id=8, name=landon7, lv=100, vipLv=8]
isVip:true
Player [id=9, name=landon8, lv=41, vipLv=9]
isVip:true
Player [id=10, name=landon9, lv=100, vipLv=5]
isVip:true
----------------------------------
Execute Script:ModifyPlayerFieldScript
Execute Script:ModifyPlayerLogicScript
Player [id=1, name=landon0, lv=93, vipLv=6]
isVip:false
Player [id=2, name=landon1, lv=37, vipLv=9]
isVip:false
Player [id=3, name=landon2, lv=25, vipLv=2]
isVip:false
Player [id=4, name=landon3, lv=78, vipLv=4]
isVip:false
Player [id=5, name=landon4, lv=10086, vipLv=11]
isVip:false
Player [id=6, name=landon5, lv=84, vipLv=1]
isVip:false
Player [id=7, name=landon6, lv=95, vipLv=4]
isVip:false
Player [id=8, name=landon7, lv=100, vipLv=8]
isVip:false
Player [id=9, name=landon8, lv=41, vipLv=9]
isVip:false
Player [id=10, name=landon9, lv=100, vipLv=5]
isVip:false
----------------------------------
execute groovy:ModifyPlayerField
Execute groovy:ModifyPlayerLogic
Player [id=1, name=landon0, lv=93, vipLv=6]
isVip:true
Player [id=2, name=landon1, lv=37, vipLv=9]
isVip:true
Player [id=3, name=landon2, lv=25, vipLv=2]
isVip:false
Player [id=4, name=landon3, lv=78, vipLv=4]
isVip:false
Player [id=5, name=landon4, lv=10086, vipLv=11]
isVip:true
Player [id=6, name=landon5, lv=84, vipLv=1]
isVip:false
Player [id=7, name=landon6, lv=99999, vipLv=15]
isVip:true
Player [id=8, name=landon7, lv=100, vipLv=8]
isVip:true
Player [id=9, name=landon8, lv=41, vipLv=9]
isVip:true
Player [id=10, name=landon9, lv=100, vipLv=5]
isVip:false
----------------------------------
posted on 2015-08-07 21:59 landon 阅读(1187) 评论(0)  编辑  收藏 所属分类: ScriptHotSwapClassLoader

只有注册用户登录后才能发表评论。


网站导航: