使用 RelProxy 提高 Java 開發效率

ImportNew發表於2015-10-31

RelProxy 旨在通過下列兩種方式提高開發效率:

可以在生產環境下修改使用者程式碼,而不需要重新載入整個應用。

提高開發效率,避免花費過多的時間載入應用且對效能不會有影響。

兩個目標都要求在你的應用中增加一些 RelProxy 程式碼,註冊成一種典型的監聽、回撥模式。這是一種“侵入”的方式。

如果你是一名Java 框架或獨立 Java 通用服務模組的開發者,可以將 RelProxy Java 嵌入到你的框架中,這樣能透明地為框架的終端使用者提供程式碼自動載入功能,只需要進行一些必要的配置,而無需呼叫 RelProxy API。

對使用 Java 版的 RelProxy,有兩種 API 可供呼叫:

JProxy 及其相關類:主要是靜態方法

Java 指令碼 API:基於介面

第二種方式更適合將 RelProxy 嵌入到你的 Java 框架中,這種方式是基於介面的,在你的 API 中無需暴露公共 RelProxy 類,因為在框架中會執行啟動程式。我將使用比較簡單的 API:JProxyScriptEngineFactory.create()。

JProxyScriptEngine 的功能與 Jproxy 相同,也就是說具有相同的方法。只是這種情況下,只需要使用介面。

一個簡單的例子是演示如何嵌入 RelProxy 的最好方式。這個例子是 RelProxy 的示例倉庫中包含的 RelProxyBuiltin(relproxy_builtin_ex 專案中)。它定義了兩個監聽器來實現註冊使用者端的程式碼,一個監聽器顯示選項(option),另一個執行選擇的行為。

這個迷你框架和示例使用 NetBeans 和 Maven 開發完成。

有兩個包:

com.innowhere.relproxy_builtin_ex :迷你框架。子包 com.innowhere.relproxy_builtin_ex.impl 只包含一個非公共的類。

com.innowhere.relproxy_builtin_ex_main :一個簡單的使用示例。

迷你框架(公共類和介面):

RelProxyBuiltinRoot.java

package com.innowhere.relproxy_builtin_ex;
import com.innowhere.relproxy_builtin_ex.impl.RelProxyBuiltinImpl;
public class RelProxyBuiltinRoot
{
    private final static RelProxyBuiltinImpl SINGLETON = new RelProxyBuiltinImpl();
    public static RelProxyBuiltin get()
    {
        return SINGLETON;
    }
}

RelProxyBuiltin.java

package com.innowhere.relproxy_builtin_ex;
import com.innowhere.relproxy.jproxy.JProxyScriptEngine;
import java.io.InputStream;
import java.io.PrintStream;
public interface RelProxyBuiltin
{
    public JProxyScriptEngine getJProxyScriptEngine();
    public void addOutputListener(OutputListener listener);
    public void removeOutputListener(OutputListener listener);
    public int getOutputListenerCount();
    public void addCommandListener(CommandListener listener);
    public void removeCommandListener(CommandListener listener);
    public int getCommandListenerCount();
    public void runLoop(InputStream in,PrintStream out);
}

OutputListener.java

package com.innowhere.relproxy_builtin_ex;
import java.io.PrintStream;
public interface OutputListener
{
    public void write(PrintStream out);
}

CommandListener.java

package com.innowhere.relproxy_builtin_ex;
import java.io.PrintStream;
public interface CommandListener
{
    public void execute(String command,String input,PrintStream out);
}

現在看一下實現細節,該類演示了怎樣簡單地內嵌 RelProxy:

RelProxyBuiltinImpl.java

package com.innowhere.relproxy_builtin_ex.impl;
import com.innowhere.relproxy.jproxy.JProxyScriptEngine;
import com.innowhere.relproxy.jproxy.JProxyScriptEngineFactory;
import com.innowhere.relproxy_builtin_ex.CommandListener;
import com.innowhere.relproxy_builtin_ex.OutputListener;
import com.innowhere.relproxy_builtin_ex.RelProxyBuiltin;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.LinkedHashSet;
import java.util.Scanner;
public class RelProxyBuiltinImpl implements RelProxyBuiltin
{
    protected JProxyScriptEngine jProxyEngine = null;
    protected LinkedHashSet<OutputListener> outListeners = new LinkedHashSet<OutputListener>();
    protected LinkedHashSet<CommandListener>  commandListeners = new LinkedHashSet<CommandListener>();
    @Override
    public JProxyScriptEngine getJProxyScriptEngine()
    {
        if (jProxyEngine == null) jProxyEngine = (JProxyScriptEngine)JProxyScriptEngineFactory.create().getScriptEngine();
        return jProxyEngine;
    }
    public JProxyScriptEngine getJProxyScriptEngineIfConfigured()
    {
        if (jProxyEngine == null || !jProxyEngine.isEnabled())
            return null;
        return jProxyEngine;
    }
    @Override
    public void addOutputListener(OutputListener listener)
    {
        JProxyScriptEngine jProxy = getJProxyScriptEngineIfConfigured();
        if (jProxy != null)
        {
            listener = jProxy.create(listener,OutputListener.class);
        }
        outListeners.add(listener);
    }
    @Override
    public void removeOutputListener(OutputListener listener)
    {
        JProxyScriptEngine jProxy = getJProxyScriptEngineIfConfigured();
        if (jProxy != null)
        {
            listener = jProxy.create(listener,OutputListener.class);
        }
        outListeners.remove(listener);
    }
    @Override
    public int getOutputListenerCount()
    {
        return outListeners.size();
    }
    @Override
    public void addCommandListener(CommandListener listener)
    {
        JProxyScriptEngine jProxy = getJProxyScriptEngineIfConfigured();
        if (jProxy != null)
        {
            listener = jProxy.create(listener,CommandListener.class);
        }
        commandListeners.add(listener);
    }
    @Override
    public void removeCommandListener(CommandListener listener)
    {
        JProxyScriptEngine jProxy = getJProxyScriptEngineIfConfigured();
        if (jProxy != null)
        {
            listener = jProxy.create(listener,CommandListener.class);
        }
        commandListeners.remove(listener);
    }
    @Override
    public int getCommandListenerCount()
    {
        return commandListeners.size();
    }
    @Override
    public void runLoop(InputStream in,PrintStream out)
    {
        Scanner scanner = new Scanner(in);
        while(true)
        {
            out.print("Enter phrase:");
            String input = scanner.nextLine();
            out.println("Command list:");
            for(OutputListener listener : outListeners)
                listener.write(out);
            out.print("Enter command (or quit):");
            String command = scanner.nextLine();
            if ("quit".equals(command))
                break;
            for(CommandListener listener : commandListeners)
                listener.execute(command,input,out);
        }
    }
}

這三個方法足以解釋怎樣啟動 RelProxy Java 引擎,怎樣簡單地使用指令監聽器來註冊熱載入。

RelProxyBuiltinImpl.java (部分)

  @Override
    public JProxyScriptEngine getJProxyScriptEngine()
    {
        if (jProxyEngine == null) jProxyEngine = (JProxyScriptEngine)JProxyScriptEngineFactory.create().getScriptEngine();
        return jProxyEngine;
    }
    public JProxyScriptEngine getJProxyScriptEngineIfConfigured()
    {
        if (jProxyEngine == null || !jProxyEngine.isEnabled())
            return null;
        return jProxyEngine;
    }
    @Override
    public void addOutputListener(OutputListener listener)
    {
        JProxyScriptEngine jProxy = getJProxyScriptEngineIfConfigured();
        if (jProxy != null)
        {
            listener = jProxy.create(listener,OutputListener.class);
        }
        outListeners.add(listener);
    }

公共方法 RelProxyBuiltin.getJProxyScriptEngine() 必須在啟動時執行,用於配置 RelProxy。如果沒有配置,RelProxy 就不起作用。

請記住,通過 create(…) 建立的代理物件需要能正確的執行 hashCode() 方法和 equals(Object) 方法,監聽器集合、監聽記錄依賴這兩個方法來區別監聽器物件。

這是基於控制檯的示例程式碼(名稱與 JUnit 類似,但確實不是 JUnit 的測試示例):

Main.java

package com.innowhere.relproxy_builtin_ex_main;
import com.innowhere.relproxy.RelProxyOnReloadListener;
import com.innowhere.relproxy.jproxy.JProxy;
import com.innowhere.relproxy.jproxy.JProxyCompilerListener;
import com.innowhere.relproxy.jproxy.JProxyConfig;
import com.innowhere.relproxy.jproxy.JProxyDiagnosticsListener;
import com.innowhere.relproxy.jproxy.JProxyInputSourceFileExcludedListener;
import com.innowhere.relproxy.jproxy.JProxyScriptEngine;
import com.innowhere.relproxy_builtin_ex.CommandListener;
import com.innowhere.relproxy_builtin_ex.RelProxyBuiltin;
import com.innowhere.relproxy_builtin_ex.RelProxyBuiltinRoot;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaFileObject;
public class Main
{
    public static void main(String[] args) throws Exception
    {
        new Main();
    }
    public Main()
    {
        // Note: NetBeans Console window works bad (no input) with Maven Test tasks http://stackoverflow.com/questions/3035351/broken-console-in-maven-project-using-netbeans
        // this is why is not a really JUnit test.
        setUp();
        try
        {
            mainTest();
        }
        finally
        {
            tearDown();
        }
        System.exit(0);
    }
    public void setUp()
    {
        URL res = this.getClass().getResource("/"); // .../target/classes/
        // Use example of RelProxy in development time:
        String inputPath = res.getFile() + "/../../src/main/java/";
        if (new File(inputPath).exists())
        {
            System.out.println("RelProxy to be enabled, development mode detected");
        }
        else
        {
            System.out.println("RelProxy disabled, production mode detected");
            return;
        }
        JProxyInputSourceFileExcludedListener excludedListener = new JProxyInputSourceFileExcludedListener()
        {
            @Override
            public boolean isExcluded(File file, File rootFolderOfSources)
            {
                String absPath = file.getAbsolutePath();
                if (file.isDirectory())
                {
                    return absPath.endsWith(File.separatorChar + "relproxy_builtin_ex");
                }
                else
                {
                    return absPath.endsWith(File.separatorChar + Main.class.getSimpleName() + ".java");
                }
            }
        };
        String classFolder = null; // Optional
        Iterable<String> compilationOptions = Arrays.asList(new String[]{"-source","1.6","-target","1.6"});
        long scanPeriod = 1000;
        RelProxyOnReloadListener proxyListener = new RelProxyOnReloadListener() {
            @Override
            public void onReload(Object objOld, Object objNew, Object proxy, Method method, Object[] args) {
                System.out.println("Reloaded " + objNew + " Calling method: " + method);
            }
        };
        JProxyCompilerListener compilerListener = new JProxyCompilerListener(){
            @Override
            public void beforeCompile(File file)
            {
                System.out.println("Before compile: " + file);
            }
            @Override
            public void afterCompile(File file)
            {
                System.out.println("After compile: " + file);
            }
        };
        JProxyDiagnosticsListener diagnosticsListener = new JProxyDiagnosticsListener()
        {
            @Override
            public void onDiagnostics(DiagnosticCollector<JavaFileObject> diagnostics)
            {
                List<Diagnostic<? extends JavaFileObject>> diagList = diagnostics.getDiagnostics();
                int i = 1;
                for (Diagnostic diagnostic : diagList)
                {
                   System.err.println("Diagnostic " + i);
                   System.err.println("  code: " + diagnostic.getCode());
                   System.err.println("  kind: " + diagnostic.getKind());
                   System.err.println("  line number: " + diagnostic.getLineNumber());
                   System.err.println("  column number: " + diagnostic.getColumnNumber());
                   System.err.println("  start position: " + diagnostic.getStartPosition());
                   System.err.println("  position: " + diagnostic.getPosition());
                   System.err.println("  end position: " + diagnostic.getEndPosition());
                   System.err.println("  source: " + diagnostic.getSource());
                   System.err.println("  message: " + diagnostic.getMessage(null));
                   i++;
                }
            }
        };
        RelProxyBuiltin rpbRoot = RelProxyBuiltinRoot.get();
        JProxyScriptEngine engine = rpbRoot.getJProxyScriptEngine();
        JProxyConfig jpConfig = JProxy.createJProxyConfig();
        jpConfig.setEnabled(true)
                .setRelProxyOnReloadListener(proxyListener)
                .setInputPath(inputPath)
                .setJProxyInputSourceFileExcludedListener(excludedListener)
                .setScanPeriod(scanPeriod)
                .setClassFolder(classFolder)
                .setCompilationOptions(compilationOptions)
                .setJProxyCompilerListener(compilerListener)
                .setJProxyDiagnosticsListener(diagnosticsListener);
        engine.init(jpConfig);
        System.out.println("RelProxy running");
    }
    public void tearDown()
    {
        RelProxyBuiltin rpbRoot = RelProxyBuiltinRoot.get();
        JProxyScriptEngine engine = rpbRoot.getJProxyScriptEngine();
        engine.stop();
        System.out.println("RelProxy stopped");
    }
    public void mainTest()
    {
        RelProxyBuiltin rpbRoot = RelProxyBuiltinRoot.get();
        TestListener listener = new TestListener();
        rpbRoot.addOutputListener(listener);
        assertTrue(rpbRoot.getOutputListenerCount() == 1);
        rpbRoot.removeOutputListener(listener);
        assertTrue(rpbRoot.getOutputListenerCount() == 0);
        rpbRoot.addOutputListener(listener);
        CommandListener commandListener = listener.getCommandListener();
        rpbRoot.addCommandListener(commandListener);
        assertTrue(rpbRoot.getCommandListenerCount() == 1);
        rpbRoot.removeCommandListener(commandListener);
        assertTrue(rpbRoot.getCommandListenerCount() == 0);
        rpbRoot.addCommandListener(commandListener);
        rpbRoot.runLoop(System.in,System.out);
    }
    private static void assertTrue(boolean res)
    {
        if (!res) throw new RuntimeException("Unexpected Error");
    }
}

看一下這段程式碼:

Main.java (部分)

 URL res = this.getClass().getResource("/"); // .../target/classes/
        // Use example of RelProxy in development time:
        String inputPath = res.getFile() + "/../../src/main/java/";
        if (new File(inputPath).exists())
        {
            System.out.println("RelProxy to be enabled, development mode detected");
        }
        else
        {
            System.out.println("RelProxy disabled, production mode detected");
            return;
        }
        JProxyInputSourceFileExcludedListener excludedListener = new JProxyInputSourceFileExcludedListener()
        {
            @Override
            public boolean isExcluded(File file, File rootFolderOfSources)
            {
                String absPath = file.getAbsolutePath();
                if (file.isDirectory())
                {
                    return absPath.endsWith(File.separatorChar + "relproxy_builtin_ex");
                }
                else
                {
                    return absPath.endsWith(File.separatorChar + Main.class.getSimpleName() + ".java");
                }
            }
        };

我們獲取並註冊應用原始碼的根目錄,該程式碼“可能”會被重新載入。

我們需要排除框架程式碼,因為這顯然不是使用者的程式碼(不需要重新載入)。此外,還需要排除 Main.java 檔案,該檔案包含了測試程式碼,也不需要重新載入,只有 TestListener.java 類(與 Main.java 在同一資料夾下)需要(必需)重新載入。

最後 TestListener.java 類包含兩個監聽器,CommandListener 的實現採用匿名內部類的方式,主要目的是為了演示。

TestListener.java

package com.innowhere.relproxy_builtin_ex_main;
import com.innowhere.relproxy_builtin_ex.CommandListener;
import com.innowhere.relproxy_builtin_ex.OutputListener;
import java.io.PrintStream;
public class TestListener implements OutputListener
{
    @Override
    public void write(PrintStream out)
    {
        out.println("uppercase");
        out.println("lowercase");
    }
    public CommandListener getCommandListener()
    {
        return new CommandListener()
        {
            @Override
            public void execute(String command,String text,PrintStream out)
            {
                if ("uppercase".equals(command))
                    out.println(text.toUpperCase());
                else if ("lowercase".equals(command))
                    out.println(text.toLowerCase());
                else
                    out.println("Unknown command:" + command);
            }
        };
    }
}

先預定義可選項,然後執行 Main 類。為了校驗 RelProxy 是否起作用,可以在不停止程式的執行的基礎上增加一個新的可選項“same”。

@Override
    public void write(PrintStream out)
    {
        out.println("uppercase");
        out.println("lowercase");
        out.println("same");  // NEW
    }
    public CommandListener getCommandListener()
    {
        return new CommandListener()
        {
            @Override
            public void execute(String command,String text,PrintStream out)
            {
                if ("uppercase".equals(command))
                    out.println(text.toUpperCase());
                else if ("lowercase".equals(command))
                    out.println(text.toLowerCase());
                else if ("same".equals(command)) // NEW
                    out.println(text); // NEW
                else
                    out.println("Unknown command:" + command);
            }
        };
    }
}

下一篇文章中將處理包含當前“same”的行為,不需要停止控制檯應用。

ItsNat web 框架可能是第一個使用 RelProxy 技術的應用(版本 v1.4)。

注意:使用 RelProxy 0.8.7 或更高的版本,這個版本在嵌入方式上做了改進。

相關文章