深入理解Java的動態編譯

前提

筆者很久之前就有個想法:參考現有的主流ORM框架的設計,造一個ORM輪子,在基本不改變使用體驗的前提下把框架依賴的大量的反射設計去掉,這些反射API構築的組件使用動態編譯加載的實例去替代,從而可以得到接近於直接使用原生JDBC的性能。於是帶着這樣的想法,深入學習Java的動態編譯。編寫本文的時候使用的是JDK11

基本原理

下面這個很眼熟的圖來源於《深入理解Java虛擬機》前端編譯與優化的章節,主要描述編譯的過程:

上圖看起來只有三步,其實每一步都有大量的步驟,下圖嘗試相對詳細地描述具體的步驟(圖比較大難以分割,直接放原圖):

實際上,僅僅對於編譯這個過程來說,開發者或者使用者不必要完全掌握其中的細節,JDK提供了一個工具包javax.tools讓使用者可以用簡易的API進行編譯(其實在大多數請下,開發者是面向業務功能開發,像編譯和打包這些細節一般直接由開發工具、MavenGradle等工具完成):

具體的使用過程包括:

  • 獲取一個javax.tools.JavaCompiler實例。
  • 基於Java文件對象初始化一個編譯任務javax.tools.JavaCompiler$CompilationTask實例。
  • CompilationTask實例執行結果代表着編譯過程的成功與否。

我們熟知的javac編譯器其實就是JavaCompiler接口的實現,在JDK11中,對應的實現類為com.sun.tools.javac.api.JavacTool。在JDK8中不存在JavaCompiler接口,具體的編譯入口類為com.sun.tools.javac.main.JavaCompiler

因為JVM裏面的Class是基於ClassLoader隔離的,所以編譯成功之後可以通過自定義的類加載器加載對應的類實例,然後就可以應用反射API進行實例化和後續的調用。

JDK動態編譯

JDK動態編譯的步驟在上一節已經清楚地說明,這裏造一個簡單的場景。假設存在一個接口如下:

package club.throwable.compile;

public interface HelloService {
    
    void sayHello(String name);
}

// 默認實現
package club.throwable.compile;

public class DefaultHelloService implements HelloService {

    @Override
    public void sayHello(String name) {
        System.out.println(String.format("%s say hello [by default]", name));
    }
}

我們可以通過字符串SOURCE_CODE定義一個類:

static String SOURCE_CODE = "package club.throwable.compile;\n" +
        "\n" +
        "public class JdkDynamicCompileHelloService implements HelloService{\n" +
        "\n" +
        "    @Override\n" +
        "    public void sayHello(String name) {\n" +
        "        System.out.println(String.format(\"%s say hello [by jdk dynamic compile]\", name));\n" +
        "    }\n" +
        "}";

// 這裏不需要定義類文件,還原類文件內容如下
package club.throwable.compile;

public class JdkDynamicCompileHelloService implements HelloService{

    @Override
    public void sayHello(String name) {
        System.out.println(String.format("%s say hello [by jdk dynamic compile]", name));
    }
}

在組裝編譯任務實例之前,還有幾項工作需要完成:

  • 內置的JavaFileObject標準實現SimpleJavaFileObject是面向類源碼文件,由於動態編譯時候輸入的是類源碼文件的內容字符串,需要自行實現JavaFileObject
  • 內置的JavaFileManager是面向類路徑下的Java源碼文件進行加載,這裏也需要自行實現JavaFileManager
  • 需要自定義一個ClassLoader實例去加載編譯出來的動態類。

實現JavaFileObject

自行實現一個JavaFileObject,其實可以簡單點直接繼承SimpleJavaFileObject,覆蓋需要用到的方法即可:

public class CharSequenceJavaFileObject extends SimpleJavaFileObject {

    public static final String CLASS_EXTENSION = ".class";

    public static final String JAVA_EXTENSION = ".java";

    private static URI fromClassName(String className) {
        try {
            return new URI(className);
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException(className, e);
        }
    }

    private ByteArrayOutputStream byteCode;
    private final CharSequence sourceCode;

    public CharSequenceJavaFileObject(String className, CharSequence sourceCode) {
        super(fromClassName(className + JAVA_EXTENSION), Kind.SOURCE);
        this.sourceCode = sourceCode;
    }

    public CharSequenceJavaFileObject(String fullClassName, Kind kind) {
        super(fromClassName(fullClassName), kind);
        this.sourceCode = null;
    }

    public CharSequenceJavaFileObject(URI uri, Kind kind) {
        super(uri, kind);
        this.sourceCode = null;
    }
    
    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
        return sourceCode;
    }

    @Override
    public InputStream openInputStream() {
        return new ByteArrayInputStream(getByteCode());
    }
    
    // 注意這個方法是編譯結果回調的OutputStream,回調成功后就能通過下面的getByteCode()方法獲取目標類編譯后的字節碼字節數組
    @Override
    public OutputStream openOutputStream() {
        return byteCode = new ByteArrayOutputStream();
    }

    public byte[] getByteCode() {
        return byteCode.toByteArray();
    }
}

如果編譯成功之後,直接通過自行添加的CharSequenceJavaFileObject#getByteCode()方法即可獲取目標類編譯后的字節碼對應的字節數組(二進制內容)。這裏的CharSequenceJavaFileObject預留了多個構造函數用於兼容原有的編譯方式。

實現ClassLoader

只要簡單繼承ClassLoader即可,關鍵是要覆蓋原來的ClassLoader#findClass()方法,用於搜索自定義的JavaFileObject實例,從而提取對應的字節碼字節數組進行裝載,為了實現這一點可以添加一個哈希表作為緩存,鍵-值分別是全類名的別名(xx.yy.MyClass形式,而非URI模式)和目標類對應的JavaFileObject實例。

public class JdkDynamicCompileClassLoader extends ClassLoader {

    public static final String CLASS_EXTENSION = ".class";

    private final Map<String, JavaFileObject> javaFileObjectMap = Maps.newConcurrentMap();

    public JdkDynamicCompileClassLoader(ClassLoader parentClassLoader) {
        super(parentClassLoader);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        JavaFileObject javaFileObject = javaFileObjectMap.get(name);
        if (null != javaFileObject) {
            CharSequenceJavaFileObject charSequenceJavaFileObject = (CharSequenceJavaFileObject) javaFileObject;
            byte[] byteCode = charSequenceJavaFileObject.getByteCode();
            return defineClass(name, byteCode, 0, byteCode.length);
        }
        return super.findClass(name);
    }

    @Nullable
    @Override
    public InputStream getResourceAsStream(String name) {
        if (name.endsWith(CLASS_EXTENSION)) {
            String qualifiedClassName = name.substring(0, name.length() - CLASS_EXTENSION.length()).replace('/', '.');
            CharSequenceJavaFileObject javaFileObject = (CharSequenceJavaFileObject) javaFileObjectMap.get(qualifiedClassName);
            if (null != javaFileObject && null != javaFileObject.getByteCode()) {
                return new ByteArrayInputStream(javaFileObject.getByteCode());
            }
        }
        return super.getResourceAsStream(name);
    }

    /**
     * 暫時存放編譯的源文件對象,key為全類名的別名(非URI模式),如club.throwable.compile.HelloService
     */
    void addJavaFileObject(String qualifiedClassName, JavaFileObject javaFileObject) {
        javaFileObjectMap.put(qualifiedClassName, javaFileObject);
    }

    Collection<JavaFileObject> listJavaFileObject() {
        return Collections.unmodifiableCollection(javaFileObjectMap.values());
    }
}

實現JavaFileManager

JavaFileManagerJava文件的抽象管理器,它用於管理常規的Java文件,但是不局限於文件,也可以管理其他來源的Java類文件數據。下面就通過實現一個自定義的JavaFileManager用於管理字符串類型的源代碼。為了簡單起見,可以直接繼承已經存在的ForwardingJavaFileManager

public class JdkDynamicCompileJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {

    private final JdkDynamicCompileClassLoader classLoader;
    private final Map<URI, JavaFileObject> javaFileObjectMap = Maps.newConcurrentMap();

    public JdkDynamicCompileJavaFileManager(JavaFileManager fileManager, JdkDynamicCompileClassLoader classLoader) {
        super(fileManager);
        this.classLoader = classLoader;
    }

    private static URI fromLocation(Location location, String packageName, String relativeName) {
        try {
            return new URI(location.getName() + '/' + packageName + '/' + relativeName);
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException(e);
        }
    }

    @Override
    public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException {
        JavaFileObject javaFileObject = javaFileObjectMap.get(fromLocation(location, packageName, relativeName));
        if (null != javaFileObject) {
            return javaFileObject;
        }
        return super.getFileForInput(location, packageName, relativeName);
    }

    /**
     * 這裡是編譯器返回的同(源)Java文件對象,替換為CharSequenceJavaFileObject實現
     */
    @Override
    public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
        JavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, kind);
        classLoader.addJavaFileObject(className, javaFileObject);
        return javaFileObject;
    }

    /**
     * 這裏覆蓋原來的類加載器
     */
    @Override
    public ClassLoader getClassLoader(Location location) {
        return classLoader;
    }

    @Override
    public String inferBinaryName(Location location, JavaFileObject file) {
        if (file instanceof CharSequenceJavaFileObject) {
            return file.getName();
        }
        return super.inferBinaryName(location, file);
    }

    @Override
    public Iterable<JavaFileObject> list(Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse) throws IOException {
        Iterable<JavaFileObject> superResult = super.list(location, packageName, kinds, recurse);
        List<JavaFileObject> result = Lists.newArrayList();
        // 這裏要區分編譯的Location以及編譯的Kind
        if (location == StandardLocation.CLASS_PATH && kinds.contains(JavaFileObject.Kind.CLASS)) {
            // .class文件以及classPath下
            for (JavaFileObject file : javaFileObjectMap.values()) {
                if (file.getKind() == JavaFileObject.Kind.CLASS && file.getName().startsWith(packageName)) {
                    result.add(file);
                }
            }
            // 這裏需要額外添加類加載器加載的所有Java文件對象
            result.addAll(classLoader.listJavaFileObject());
        } else if (location == StandardLocation.SOURCE_PATH && kinds.contains(JavaFileObject.Kind.SOURCE)) {
            // .java文件以及編譯路徑下
            for (JavaFileObject file : javaFileObjectMap.values()) {
                if (file.getKind() == JavaFileObject.Kind.SOURCE && file.getName().startsWith(packageName)) {
                    result.add(file);
                }
            }
        }
        for (JavaFileObject javaFileObject : superResult) {
            result.add(javaFileObject);
        }
        return result;
    }

    /**
     * 自定義方法,用於添加和緩存待編譯的源文件對象
     */
    public void addJavaFileObject(Location location, String packageName, String relativeName, JavaFileObject javaFileObject) {
        javaFileObjectMap.put(fromLocation(location, packageName, relativeName), javaFileObject);
    }
}

注意在這個類中引入了自定義類加載器JdkDynamicCompileClassLoader,目的是為了實現JavaFileObject實例的共享以及為文件管理器提供類加載器實例。

動態編譯和運行

前置準備工作完成,我們可以通過JavaCompiler去編譯這個前面提到的字符串,為了字節碼的兼容性更好,編譯的時候可以指定稍低的JDK版本例如1.6

public class Client {

    static String SOURCE_CODE = "package club.throwable.compile;\n" +
            "\n" +
            "public class JdkDynamicCompileHelloService implements HelloService{\n" +
            "\n" +
            "    @Override\n" +
            "    public void sayHello(String name) {\n" +
            "        System.out.println(String.format(\"%s say hello [by jdk dynamic compile]\", name));\n" +
            "    }\n" +
            "}";

    /**
     * 編譯診斷收集器
     */
    static DiagnosticCollector<JavaFileObject> DIAGNOSTIC_COLLECTOR = new DiagnosticCollector<>();

    public static void main(String[] args) throws Exception {
        // 獲取系統編譯器實例
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        // 設置編譯參數 - 指定編譯版本為JDK1.6以提高兼容性
        List<String> options = new ArrayList<>();
        options.add("-source");
        options.add("1.6");
        options.add("-target");
        options.add("1.6");
        // 獲取標準的Java文件管理器實例
        StandardJavaFileManager manager = compiler.getStandardFileManager(DIAGNOSTIC_COLLECTOR, null, null);
        // 初始化自定義類加載器
        JdkDynamicCompileClassLoader classLoader = new JdkDynamicCompileClassLoader(Thread.currentThread().getContextClassLoader());
        // 初始化自定義Java文件管理器實例
        JdkDynamicCompileJavaFileManager fileManager = new JdkDynamicCompileJavaFileManager(manager, classLoader);
        String packageName = "club.throwable.compile";
        String className = "JdkDynamicCompileHelloService";
        String qualifiedName = packageName + "." + className;
        // 構建Java源文件實例
        CharSequenceJavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, SOURCE_CODE);
        // 添加Java源文件實例到自定義Java文件管理器實例中
        fileManager.addJavaFileObject(
                StandardLocation.SOURCE_PATH,
                packageName,
                className + CharSequenceJavaFileObject.JAVA_EXTENSION,
                javaFileObject
        );
        // 初始化一個編譯任務實例
        JavaCompiler.CompilationTask compilationTask = compiler.getTask(
                null,
                fileManager,
                DIAGNOSTIC_COLLECTOR,
                options,
                null,
                Lists.newArrayList(javaFileObject)
        );
        // 執行編譯任務
        Boolean result = compilationTask.call();
        System.out.println(String.format("編譯[%s]結果:%s", qualifiedName, result));
        Class<?> klass = classLoader.loadClass(qualifiedName);
        HelloService instance = (HelloService) klass.getDeclaredConstructor().newInstance();
        instance.sayHello("throwable");
    }
}

輸出結果如下:

編譯[club.throwable.compile.JdkDynamicCompileHelloService]結果:true
throwable say hello [by jdk dynamic compile]

可見通過了字符串的類源碼,實現了動態編譯、類加載、反射實例化以及最終的方法調用。另外,編譯過程的診斷信息可以通過DiagnosticCollector實例獲取。為了復用,這裏可以把JDK動態編譯的過程抽取到一個方法中:

public final class JdkCompiler {

    static DiagnosticCollector<JavaFileObject> DIAGNOSTIC_COLLECTOR = new DiagnosticCollector<>();

    @SuppressWarnings("unchecked")
    public static <T> T compile(String packageName,
                                String className,
                                String sourceCode,
                                Class<?>[] constructorParamTypes,
                                Object[] constructorParams) throws Exception {
        // 獲取系統編譯器實例
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        // 設置編譯參數
        List<String> options = new ArrayList<>();
        options.add("-source");
        options.add("1.6");
        options.add("-target");
        options.add("1.6");
        // 獲取標準的Java文件管理器實例
        StandardJavaFileManager manager = compiler.getStandardFileManager(DIAGNOSTIC_COLLECTOR, null, null);
        // 初始化自定義類加載器
        JdkDynamicCompileClassLoader classLoader = new JdkDynamicCompileClassLoader(Thread.currentThread().getContextClassLoader());
        // 初始化自定義Java文件管理器實例
        JdkDynamicCompileJavaFileManager fileManager = new JdkDynamicCompileJavaFileManager(manager, classLoader);
        String qualifiedName = packageName + "." + className;
        // 構建Java源文件實例
        CharSequenceJavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, sourceCode);
        // 添加Java源文件實例到自定義Java文件管理器實例中
        fileManager.addJavaFileObject(
                StandardLocation.SOURCE_PATH,
                packageName,
                className + CharSequenceJavaFileObject.JAVA_EXTENSION,
                javaFileObject
        );
        // 初始化一個編譯任務實例
        JavaCompiler.CompilationTask compilationTask = compiler.getTask(
                null,
                fileManager,
                DIAGNOSTIC_COLLECTOR,
                options,
                null,
                Lists.newArrayList(javaFileObject)
        );
        Boolean result = compilationTask.call();
        System.out.println(String.format("編譯[%s]結果:%s", qualifiedName, result));
        Class<?> klass = classLoader.loadClass(qualifiedName);
        return (T) klass.getDeclaredConstructor(constructorParamTypes).newInstance(constructorParams);
    }
}

Javassist動態編譯

既然有JDK的動態編譯,為什麼還存在Javassist這樣的字節碼增強工具?撇開性能或者效率層面,JDK動態編譯存在比較大的局限性,比較明顯的一點就是無法完成字節碼插樁,換言之就是無法基於原有的類和方法進行修飾或者增強,但是Javassist可以做到。再者,Javassist提供的APIJDK反射的API十分相近,如果反射平時用得比較熟練,Javassist的上手也就變得比較簡單。這裏僅僅列舉一個增強前面提到的DefaultHelloService的例子,先引入依賴:

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.27.0-GA</version>
</dependency>

編碼如下:

public class JavassistClient {

    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.get("club.throwable.compile.DefaultHelloService");
        CtMethod ctMethod = cc.getDeclaredMethod("sayHello", new CtClass[]{pool.get("java.lang.String")});
        ctMethod.insertBefore("System.out.println(\"insert before by Javassist\");");
        ctMethod.insertAfter("System.out.println(\"insert after by Javassist\");");
        Class<?> klass = cc.toClass();
        System.out.println(klass.getName());
        HelloService helloService = (HelloService) klass.getDeclaredConstructor().newInstance();
        helloService.sayHello("throwable");
    }
}

輸出結果如下:

club.throwable.compile.DefaultHelloService
insert before by Javassist
throwable say hello [by default]
insert after by Javassist

Javaassist這個單詞其實是JavaAssist兩個單詞拼接在一起,意為Java助手,是一個Java字節碼增強類庫:

  • 可以基於已經存在的類進行字節碼增強,例如修改已經存在的方法、變量,甚至是直接在原有的類中添加新的方法等。
  • 可以完全像積木拼接一樣,動態拼出一個全新的類。

不像ASMASM的學習曲線比較陡峭,屬於相對底層的字節碼操作類庫,當然從性能上來看ASM對字節碼增強的效率遠高於其他高層次封裝的框架)那樣需要對字節碼編程十分了解,Javaassist降低了字節碼增強功能的入門難度。

進階例子

現在定義一個接口MysqlInfoMapper,用於動態執行一條已知的SQL,很簡單,就是查詢MySQL的系統表mysql裏面的用戶信息SELECT Host,User FROM mysql.user

@Data
public class MysqlUser {

    private String host;
    private String user;
}

public interface MysqlInfoMapper {

    List<MysqlUser> selectAllMysqlUsers();
}

假設現在只提供一個MySQL的驅動包(mysql:mysql-connector-java:jar:8.0.20),暫時不能依賴任何高層次的框架,要動態實現MysqlInfoMapper接口,優先整理需要的組件:

  • 需要一個連接管理器去管理MySQL的連接。
  • 需要一個SQL執行器用於執行查詢SQL
  • 需要一個結果處理器去提取和轉換查詢結果。

為了簡單起見,筆者在定義這三個組件接口的時候順便在接口中通過單例進行實現(部分配置完全寫死):

// 連接管理器
public interface ConnectionManager {

    String USER_NAME = "root";

    String PASS_WORD = "root";

    String URL = "jdbc:mysql://localhost:3306/mysql?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8&useSSL=false";

    Connection newConnection() throws SQLException;

    void closeConnection(Connection connection);

    ConnectionManager X = new ConnectionManager() {

        @Override
        public Connection newConnection() throws SQLException {
            return DriverManager.getConnection(URL, USER_NAME, PASS_WORD);
        }

        @Override
        public void closeConnection(Connection connection) {
            try {
                connection.close();
            } catch (Exception ignore) {

            }
        }
    };
}

// 執行器
public interface SqlExecutor {

    ResultSet execute(Connection connection, String sql) throws SQLException;

    SqlExecutor X = new SqlExecutor() {

        @Override
        public ResultSet execute(Connection connection, String sql) throws SQLException {
            Statement statement = connection.createStatement();
            statement.execute(sql);
            return statement.getResultSet();
        }
    };
}

// 結果處理器
public interface ResultHandler<T> {

    T handleResultSet(ResultSet resultSet) throws SQLException;

    ResultHandler<List<MysqlUser>> X = new ResultHandler<List<MysqlUser>>() {
        @Override
        public List<MysqlUser> handleResultSet(ResultSet resultSet) throws SQLException {
            try {
                List<MysqlUser> result = Lists.newArrayList();
                while (resultSet.next()) {
                    MysqlUser item = new MysqlUser();
                    item.setHost(resultSet.getString("Host"));
                    item.setUser(resultSet.getString("User"));
                    result.add(item);
                }
                return result;
            } finally {
                resultSet.close();
            }
        }
    };
}

接着需要動態編譯MysqlInfoMapper的實現類,它的源文件的字符串內容如下(注意不要在類路徑下新建這個DefaultMysqlInfoMapper類):

package club.throwable.compile;
import java.sql.Connection;
import java.sql.ResultSet;
import java.util.List;

public class DefaultMysqlInfoMapper implements MysqlInfoMapper {

    private final ConnectionManager connectionManager;
    private final SqlExecutor sqlExecutor;
    private final ResultHandler resultHandler;
    private final String sql;

    public DefaultMysqlInfoMapper(ConnectionManager connectionManager,
                                  SqlExecutor sqlExecutor,
                                  ResultHandler resultHandler,
                                  String sql) {
        this.connectionManager = connectionManager;
        this.sqlExecutor = sqlExecutor;
        this.resultHandler = resultHandler;
        this.sql = sql;
    }

    @Override
    public List<MysqlUser> selectAllMysqlUsers() {
        try {
            Connection connection = connectionManager.newConnection();
            try {
                ResultSet resultSet = sqlExecutor.execute(connection, sql);
                return (List<MysqlUser>) resultHandler.handleResultSet(resultSet);
            } finally {
                connectionManager.closeConnection(connection);
            }
        } catch (Exception e) {
            // 暫時忽略異常處理,統一封裝為IllegalStateException
            throw new IllegalStateException(e);
        }
    }
}

然後編寫一個客戶端進行動態編譯和執行:

public class MysqlInfoClient {

    static String SOURCE_CODE = "package club.throwable.compile;\n" +
            "import java.sql.Connection;\n" +
            "import java.sql.ResultSet;\n" +
            "import java.util.List;\n" +
            "\n" +
            "public class DefaultMysqlInfoMapper implements MysqlInfoMapper {\n" +
            "\n" +
            "    private final ConnectionManager connectionManager;\n" +
            "    private final SqlExecutor sqlExecutor;\n" +
            "    private final ResultHandler resultHandler;\n" +
            "    private final String sql;\n" +
            "\n" +
            "    public DefaultMysqlInfoMapper(ConnectionManager connectionManager,\n" +
            "                                  SqlExecutor sqlExecutor,\n" +
            "                                  ResultHandler resultHandler,\n" +
            "                                  String sql) {\n" +
            "        this.connectionManager = connectionManager;\n" +
            "        this.sqlExecutor = sqlExecutor;\n" +
            "        this.resultHandler = resultHandler;\n" +
            "        this.sql = sql;\n" +
            "    }\n" +
            "\n" +
            "    @Override\n" +
            "    public List<MysqlUser> selectAllMysqlUsers() {\n" +
            "        try {\n" +
            "            Connection connection = connectionManager.newConnection();\n" +
            "            try {\n" +
            "                ResultSet resultSet = sqlExecutor.execute(connection, sql);\n" +
            "                return (List<MysqlUser>) resultHandler.handleResultSet(resultSet);\n" +
            "            } finally {\n" +
            "                connectionManager.closeConnection(connection);\n" +
            "            }\n" +
            "        } catch (Exception e) {\n" +
            "            // 暫時忽略異常處理,統一封裝為IllegalStateException\n" +
            "            throw new IllegalStateException(e);\n" +
            "        }\n" +
            "    }\n" +
            "}\n";

    static String SQL = "SELECT Host,User FROM mysql.user";

    public static void main(String[] args) throws Exception {
        MysqlInfoMapper mysqlInfoMapper = JdkCompiler.compile(
                "club.throwable.compile",
                "DefaultMysqlInfoMapper",
                SOURCE_CODE,
                new Class[]{ConnectionManager.class, SqlExecutor.class, ResultHandler.class, String.class},
                new Object[]{ConnectionManager.X, SqlExecutor.X, ResultHandler.X, SQL});
        System.out.println(JSON.toJSONString(mysqlInfoMapper.selectAllMysqlUsers()));
    }
}

最終的輸出結果是:

編譯[club.throwable.compile.DefaultMysqlInfoMapper]結果:true
[{"host":"%","user":"canal"},{"host":"%","user":"doge"},{"host":"localhost","user":"mysql.infoschema"},{"host":"localhost","user":"mysql.session"},{"host":"localhost","user":"mysql.sys"},{"host":"localhost","user":"root"}]

然後筆者查看本地安裝的MySQL中的結果,驗證該查詢結果是正確的。

這裏筆者為了簡化整個例子,沒有在MysqlInfoMapper#selectAllMysqlUsers()方法中添加查詢參數,可以嘗試一下查詢的SQLSELECT Host,User FROM mysql.user WHERE User = 'xxx'場景下的編碼實現。

如果把動態實現的DefaultMysqlInfoMapper註冊到IOC容器中,就可以實現MysqlInfoMapper按照類型自動裝配。
如果把SQL和參數處理可以抽離到單獨的文件中,並且實現一個對應的文件解析器,那麼就可以把類文件和SQL隔離,MybatisHibernate都是這樣做的。

小結

動態編譯或者更底層的面向字節碼層面的編程,其實是一個十分有挑戰性但是可以創造無限可能的領域,本文只是簡單分析了一下Java源碼編譯的過程,並且通過一些簡單的例子進行動態編譯的模擬,離使用於實際應用中還有不少距離,後面需要花更多的時間去分析一下相關領域的知識。

參考資料:

  • JDK11部分源碼
  • 《深入理解Java虛擬機 – 3rd》
  • Javassist

(本文完 c-4-d e-a-20200606 0:23)

技術公眾號(《Throwable文摘》),不定期推送筆者原創技術文章(絕不抄襲或者轉載):

娛樂公眾號(《天天沙雕》),甄選奇趣沙雕圖文和視頻不定期推送,緩解生活工作壓力:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!

Python 圖像處理 OpenCV (7):圖像平滑(濾波)處理

前文傳送門:

「Python 圖像處理 OpenCV (1):入門」

「Python 圖像處理 OpenCV (2):像素處理與 Numpy 操作以及 Matplotlib 显示圖像」

「Python 圖像處理 OpenCV (3):圖像屬性、圖像感興趣 ROI 區域及通道處理」

「Python 圖像處理 OpenCV (4):圖像算數運算以及修改顏色空間」

「Python 圖像處理 OpenCV (5):圖像的幾何變換」

「Python 圖像處理 OpenCV (6):圖像的閾值處理」

1. 引言

第一件事情還是先做名詞解釋,圖像平滑到底是個啥?

從字面意思理解貌似圖像平滑好像是在說圖像滑動。

emmmmmmmmmmmmmmm。。。。

其實半毛錢關係也沒有,圖像平滑技術通常也被成為圖像濾波技術(這個名字看到可能大家會有點感覺)。

每一幅圖像都包含某種程度的噪聲,噪聲可以理解為由一種或者多種原因造成的灰度值的隨機變化,如由光子通量的隨機性造成的噪聲等等。

而圖像平滑技術或者是圖像濾波技術就是用來處理圖像上的噪聲,其中,能夠具備邊緣保持作用的圖像平滑處理,成為了大家關注的重點。

這不廢話,處理個圖片降噪,結果把整個圖像搞的跟玻璃上糊上了一層水霧一樣,這種降噪有啥意義。

本文會介紹 OpenCV 中提供的圖像平滑的 4 個算法:

  • 均值濾波
  • 方框濾波
  • 高斯濾波
  • 中值濾波

下面開始一個一個看吧:)

先給出一個給馬里奧加噪聲的程序,程序來源於楊老師的博客:https://blog.csdn.net/Eastmount/article/details/82216380 ,完整代碼如下:

import cv2 as cv
import numpy as np

# 讀取圖片
img = cv.imread("maliao.jpg", cv.IMREAD_UNCHANGED)
rows, cols, chn = img.shape

# 加噪聲
for i in range(5000):
    x = np.random.randint(0, rows)
    y = np.random.randint(0, cols)
    img[x, y, :] = 255

cv.imshow("noise", img)

# 圖像保存
cv.imwrite("maliao_noise.jpg", img)

# 等待显示
cv.waitKey()
cv.destroyAllWindows()

上面這段程序實際上是在圖片上隨機加了 5000 個白點,這個噪聲真的是夠大的了。

2. 2D 圖像卷積

在介紹濾波之前先簡單介紹下 2D 圖像卷積,圖像卷積其實就是圖像過濾。

圖像過濾的時候可以使用各種低通濾波器( LPF ),高通濾波器( HPF )等對圖像進行過濾。

低通濾波器( LPF )有助於消除噪聲,但是會使圖像模糊。

高通濾波器( HPF )有助於在圖像中找到邊緣。

OpenCV 為我們提供了一個函數 filter2D() 來將內核與圖像進行卷積。

我們嘗試對圖像進行平均濾波, 5 x 5 平均濾波器內核如下:

\[ K = \frac{1}{25} \begin{bmatrix} 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \end{bmatrix} \]

具體操作如下:

我們保持這個內核在一個像素上,將所有低於這個內核的 25 個像素相加,取其平均值,然後用新的平均值替換中心像素。它將對圖像中的所有像素繼續此操作,完整的示例代碼如下:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

# 讀取圖片
img = cv.imread("maliao_noise.jpg", cv.IMREAD_UNCHANGED)
rgb_img = cv.cvtColor(img, cv.COLOR_BGR2RGB)

kernel = np.ones((5,5),np.float32)/25

dst = cv.filter2D(rgb_img, -1, kernel)

titles = ['Source Image', 'filter2D Image']
images = [rgb_img, dst]

for i in range(2):
    plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])

plt.show()

可以看到,噪點確實去除掉了,就是圖片變得模糊起來。

3. 均值濾波

均值濾波是指任意一點的像素值,都是周圍 N * M 個像素值的均值。

其實均值濾波和上面的那個圖像卷積的示例,做了同樣的事情,我只是用 filter2D() 這個方法手動完成了均值濾波,實際上 OpenCV 為我們提供了專門的均值濾波的方法,前面圖像卷積沒有看明白的同學,可以再一遍均值濾波,我盡量把這個事情整的明白的。

還是來畫個圖吧:

中間那個紅色的方框裏面的值,是周圍 25 個格子區域中的像素的和去除以 25 ,這個公式是下面這樣的:

\[ K = \frac{1}{25} \begin{bmatrix} 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \end{bmatrix} \]

我為了偷懶,所有的格子裏面的像素值都寫成 1 ,畢竟 n / n 永遠都等於 1 ,快誇我機智。

上面這個 5 * 5 的矩陣稱為核,針對原始圖像內的像素點,採用核進行處理,得到結果圖像。

這個核我們可以自定義大小,比如 5 * 5 ,3 * 3 , 10 * 10 等等,具體定義多大完全看療效。

OpenCV 為我提供了 blur() 方法用作實現均值濾波,原函數如下:

def blur(src, ksize, dst=None, anchor=None, borderType=None)
  • kSize: 內核參數,其實就是圖片進行卷積的時候相乘的那個矩陣,具體的卷積是如何算的,網上有很多,我這裏就不介紹了,所得到的圖像是模糊的,而且圖像其實是按照原來的比例缺少了(原圖像-內核參數+1)^2 個單元格。
  • anchor: Point 類型,即錨點,有默認值 Point(-1, -1) ,當坐標為負值,就表示取核的中心。
  • borderType: Int 類型,用於推斷圖像外部像素的某種邊界模式,有默認值 BORDER_DEFAULT 。

接下來是均值濾波的示例代碼:

import cv2 as cv
import matplotlib.pyplot as plt

# 讀取圖片
img = cv.imread("maliao_noise.jpg", cv.IMREAD_UNCHANGED)
rgb_img = cv.cvtColor(img, cv.COLOR_BGR2RGB)

# 均值濾波
blur_img = cv.blur(rgb_img, (3, 3))
# blur_img = cv.blur(img, (5, 5))
# blur_img = cv.blur(img, (10, 10))
# blur_img = cv.blur(img, (20, 20))

titles = ['Source Image', 'Blur Image']
images = [rgb_img, blur_img]

for i in range(2):
    plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])

plt.show()

這個降噪的效果好像沒有前面 2D 卷積的那個降噪效果好,但是圖像更為清晰,因為我在這個示例中使用了更小的核 3 * 3 的核,順便我也試了下大核,比如代碼中註釋掉的 10 * 10 的核或者 20 * 20 的核,實時證明,核越大降噪效果越好,但是相反的是圖像會越模糊。

4. 方框濾波

方框濾波和均值濾波核基本一致,其中的區別是需不需要進行歸一化處理。

什麼是歸一化處理等下再說,我們先看方框濾波的原函數:

def boxFilter(src, ddepth, ksize, dst=None, anchor=None, normalize=None, borderType=None)
  • src: 原始圖像。
  • ddepth: Int 類型,目標圖像深度,通常用 -1 表示與原始圖像一致。
  • kSize: 內核參數。
  • dst: 輸出與 src 大小和類型相同的圖像。
  • anchor: Point 類型,即錨點,有默認值 Point(-1, -1) 。
  • normalize: Int 類型,表示是否對目標圖像進行歸一化處理。

當 normalize 為 true 時,需要執行均值化處理。

當 normalize 為 false 時,不進行均值化處理,實際上是求周圍各像素的和,很容易發生溢出,溢出時均為白色,對應像素值為 255 。

完整示例代碼如下:

import cv2 as cv
import matplotlib.pyplot as plt

# 讀取圖片
img = cv.imread('maliao_noise.jpg')
source = cv.cvtColor(img, cv.COLOR_BGR2RGB)

# 方框濾波
result = cv.boxFilter(source, -1, (5, 5), normalize = 1)

# 显示圖形
titles = ['Source Image', 'BoxFilter Image']
images = [source, result]

for i in range(2):
    plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])

plt.show()

當我們把 normalize 的屬性設為 0 時,不進行歸一化處理,結果就變成了下面這個樣子:

5. 高斯濾波

為了克服簡單局部平均法的弊端(圖像模糊),目前已提出許多保持邊緣、細節的局部平滑算法。它們的出發點都集中在如何選擇鄰域的大小、形狀和方向、參數加平均及鄰域各店的權重係數等。

在高斯濾波的方法中,實際上是把卷積核換成了高斯核,那麼什麼是高斯核呢?

簡單來講就是方框還是那個方框,原來每個方框裏面的權是相等的,大家最後取平均,現在變成了高斯分佈的,方框中心的那個權值最大,其餘方框根據距離中心元素的距離遞減,構成一個高斯小山包,這樣取到的值就變成了加權平均。

下圖是所示的是 3 * 3 和 5 * 5 領域的高斯核。

高斯濾波是在 OpenCV 中是由 GaussianBlur() 方法進行實現的,它的原函數如下:

def GaussianBlur(src, ksize, sigmaX, dst=None, sigmaY=None, borderType=None)
  • sigmaX: 表示 X 方向方差。

這裏需要注意的是 ksize 核大小,在高斯核當中,核 (N, N) 必須是奇數, X 方向方差主要控制權重。

完整的示例代碼如下:

import cv2 as cv
import matplotlib.pyplot as plt

# 讀取圖片
img = cv.imread('maliao_noise.jpg')
source = cv.cvtColor(img, cv.COLOR_BGR2RGB)

# 方框濾波
result = cv.GaussianBlur(source, (3, 3), 0)

# 显示圖形
titles = ['Source Image', 'GaussianBlur Image']
images = [source, result]

for i in range(2):
    plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])

plt.show()

6. 中值濾波

在使用鄰域平均法去噪的同時也使得邊界變得模糊。

而中值濾波是非線性的圖像處理方法,在去噪的同時可以兼顧到邊界信息的保留。

中值濾波具體的做法是選一個含有奇數點的窗口 W ,將這個窗口在圖像上掃描,把窗口中所含的像素點按灰度級的升或降序排列,取位於中間的灰度值來代替該點的灰度值。

下圖是一個一維的窗口的濾波過程:

在 OpenCV 中,主要是通過調用 medianBlur() 來實現中值濾波,它的原函數如下:

def medianBlur(src, ksize, dst=None)

中值濾波的核心數和高斯濾波的核心數一樣,必須要是大於 1 的奇數。

示例代碼如下:

import cv2 as cv
import matplotlib.pyplot as plt

# 讀取圖片
img = cv.imread('maliao_noise.jpg')
source = cv.cvtColor(img, cv.COLOR_BGR2RGB)

# 方框濾波
result = cv.medianBlur(source, 3)

# 显示圖形
titles = ['Source Image', 'medianBlur Image']
images = [source, result]

for i in range(2):
    plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])

plt.show()

可以明顯看到,目前中值濾波是對原圖像降噪后還原度最高的,常用的中值濾波的圖形除了可以使用方框,還有十字形、圓形和環形,不同形狀的窗口產生不同的濾波效果。

方形和圓形窗口適合外輪廓線較長的物體圖像,而十字形窗口對有尖頂角狀的圖像效果好。

對於一些細節較多的複雜圖像,可以多次使用不同的中值濾波。

7. 示例代碼

如果有需要獲取源碼的同學可以在公眾號回復「OpenCV」進行獲取。

8. 參考

https://blog.csdn.net/Eastmount/article/details/82216380

http://www.woshicver.com/

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

單獨囚禁在水池2年 日本311倖存海豚抑鬱離世

摘錄自2020年4月15日鏡週刊報導

國際動保組織「海豚計畫」(Dolphin project)報導指出,日本寬吻海豚「Honey」於2005年在日本太地町被捕獲,自此生活在千葉線犬吠埼海洋公園僅80平方公尺大的水池中,與另一隻海豚、46隻企鵝及數百種魚類一起生活。

311大地震後面臨觀光衰退和建築物老舊,2008年11月犬吠埼海洋公園被爆出營運困難負債,正尋找買家,去年初終於轉賣,但如今整個園區卻呈現廢棄狀態,也沒有遊客造訪,僅安排員工前來餵食。另一隻海豚於2017年死亡,「Honey」自此單獨每天在這廢棄的水池中游來游去,對習性群居的海豚而言,這無疑是極大的折磨。

動保組織「海豚計畫」曾試圖聯繫買下海豚「Honey」,盼為牠找到合適的居所安享晚年,但為時已晚,3月初「Honey」狀況已經不太好,3月29日牠因阻塞性腸炎,死在待了泰半生的水池。

國際新聞
日本
核災
展示動物
動物福利

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!

德動物園面臨斷炊 列安樂死名單

摘錄自2020年4月16日醒報報導

自德國於3月15日下達封城令後,遊客銳減,新明斯特動物園財務吃緊,為了解決動物捱餓的問題,園方已列出緊急應變方案,考慮將部分動物安樂死,並宰殺作為其他動物的食物。

柏林動物園協會(VdZ)強調,動物園在休館期間無法大幅削減開支,每日仍須支出一定人力與金錢成本餵食並照顧動物,維持宜居的環境及溫度條件,一旦失去門票收入,便面臨龐大財務壓力。德國政府已推出高達7500億歐元的紓困方案,VdZ也已籲請總理梅克爾撥款1億歐元,作為動物園產業的緊急支應金。

國際新聞
德國
動物園
展示動物
動物福利

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

4歲男童閒晃突遇野豬群襲擊 身體遭啃食送醫不治

摘錄自2020年4月24日聯合報報導

印度近期發生一起驚悚案件,一位4歲男童21日在家中附近閒晃玩耍時,突遇野豬群襲擊,被拖進一處垃圾場中攻擊啃咬,事後警方發現男童時,他全身多處已被豬群啃食,直到男童父母到場後才識別出他的身份,而男童送醫後則宣告不治。

根據英國「太陽報」報導,男童哈沙(V. Harshavardhan)居住在印度海得拉巴(Hyderabad)的賽義達巴德(Saidabad),當地居民曾多次向地方政府投訴,反應受到野豬群干擾,當地人表示沒有辦法阻止這些野豬瘋狂奔跑,雖然野豬已經被趕走,但牠們還是會找到路回來。

生活環境
國際新聞
印度
野豬

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

新發現 澳科學家在2009年南極冰核中發現微塑膠 附近磷蝦恐吃下肚

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

※回頭車貨運收費標準

科學家在北義空污粒子上檢出新冠病毒 傳播距離、病毒是否存活仍待釐清

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!

巴哥食慾減弱呈陽性反應恐為全美首隻確診寵物犬

摘錄自2020年4月29日自由時報報導

美國北卡羅萊納州有一隻巴哥犬被檢測出對武漢肺炎病毒呈陽性反應,恐為美國第一隻寵物犬確診案例。

《NBC》報導,該隻名叫溫斯頓(Winston)的巴哥其主人家庭有多人確診,男女主人和兒子均呈陽性反應,女兒、另一隻狗以及寵物貓則呈陰性反應。女主人麥可萊恩(Heather McLean)表示,溫斯頓有輕微症狀,早上沒有食慾。報導指出,該隻巴哥的家庭成員還透露,狗狗會舔遍所有的餐盤,然後跟主人一起睡覺。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

2020第一季史上第二熱 全年溫度預測、海面上升趨勢一次看

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

荷蘭首例水鼬確診武漢肺炎 動福團體呼籲終結皮草貿易

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!