Java HotSwap Ⅲ-Script/Groovy

Java HotSwap Ⅲ-Script/Groovy
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
----------------------------------

你可能感兴趣的:(Java HotSwap Ⅲ-Script/Groovy)