在实用程序 class 中反映方法并在 Java 中使用可变参数调用它们

Reflecting methods in a utility class and invoking them with varargs in Java

我在 Java 中构建了一个 _VERY_ 基本实用程序 class 来处理数据库操作(连接检索、插入等),如下所示:

// define the package name
package com.foo.bar.helpers;

// import all needed resources
import com.foo.bar.helpers.database.MySQL;
import com.foo.bar.helpers.database.SQLite;
import java.lang.reflect.Method;
import java.sql.Array;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;

/**
 * database
 * @author John Doe <...>
 */
class Database {
    // private constructor to prevent instantiation
    private Database() throws InstantiationException {
        // throw the appropriate exception
        throw new InstantiationException();
    }

    // database classes
    public static final String SQLITE_CLASS = SQLite.class.getCanonicalName();
    public static final String MYSQL_CLASS = MySQL.class.getCanonicalName();

    /**
     * returns a connection to the database using a set of parameters
     * @param parameters the connection parameters
     * @return a connection to the database
     * @author John Doe <...>
     */
    public static Connection getConnection(Object... parameters) {
        Connection output = null;

        try {
            if (parameters.length > 0) {
                // create an instance of the target class
                Class<?> target_class = Class.forName(parameters[0].getClass().getCanonicalName());

                // remove the first parameter (database class)
                Object[] class_parameters = Arrays.copyOfRange(parameters, 1, parameters.length);

                // retrieve the class type for each parameter
                Class<?>[] class_types = new Class[class_parameters.length];

                for (int i = 0; i < class_parameters.length; i++) {
                    class_types[i] = class_parameters[i].getClass();
                }

                // reflect the target class method
                Method class_method = target_class.getDeclaredMethod("getConnection", class_types);

                // output the database connection
                output = (Connection) class_method.invoke(null, class_parameters);
            } else {
                throw new Throwable("unable to establish a connection with the database (no parameters were provided)");
            }
        } catch (Throwable e) {
            // print the stack trace
            e.printStackTrace();
        }

        return output;
    }
}

除了数据库助手,我还有两个数据库连接器(MySQL 和 SQLite),如下所示(显示 MySQL 连接器):

// define the package name
package com.foo.bar.helpers.database;

// import all needed resources
import com.foo.bar.helpers.Configuration;
import com.foo.bar.helpers.Log;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.HashMap;

/**
 * MySQL
 * @author John Doe <....>
 */
public class MySQL {
    // private constructor to prevent instantiation
    private MySQL() throws InstantiationException {
        // throw the appropriate exception
        throw new InstantiationException();
    }

    // connection key
    public static final String CONNECTION_KEY = "mysql";

    // default connection profile
    public static final String DEFAULT_CONNECTION_PROFILE = "default";

    /**
     * returns a connection to the database
     * @return a connection to the database
     * @author John Doe <....>
     */
    public static Connection getConnection() {
        Connection output = null;

        try {
            // compose the database connection profile key
            String profile_key = String.format("%s_%s", CONNECTION_KEY, DEFAULT_CONNECTION_PROFILE);

            // retrieve the database connection profile keyset
            HashMap<String, String> keyset = Configuration.getConfiguration(profile_key);

            // output the database connection
            output = DriverManager.getConnection(String.format("jdbc:mysql://%s:%s/%s?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC", keyset.get("host"), keyset.get("port"), keyset.get("schema")), keyset.get("username"), keyset.get("password"));
        } catch (Throwable e) {
            Log.error(MySQL.class, e);
        }

        return output;
    }

    /**
     * returns a connection to the database
     * @param profile the database configuration profile
     * @return a connection to the database
     * @author John Doe <....>
     */
    public static Connection getConnection(String profile) {
        Connection output = null;

        try {
            // compose the database connection profile key
            String profile_key = String.format("%s_%s", CONNECTION_KEY, profile);

            // retrieve the database connection profile keyset
            HashMap<String, String> keyset = Configuration.getConfiguration(profile_key);

            // output the database connection
            output = DriverManager.getConnection(String.format("jdbc:mysql://%s:%s/%s?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC", keyset.get("host"), keyset.get("port"), keyset.get("schema")), keyset.get("username"), keyset.get("password"));
        } catch (Throwable e) {
            Log.error(MySQL.class, e);
        }

        return output;
    }

    /**
     * returns a connection to the database
     * @param host the database host
     * @param port the database port
     * @param schema the database schema
     * @param username the database username
     * @param password the database user password
     * @return a connection to the database
     * @author John Doe <....>
     */
    public static Connection getConnection(String host, int port, String schema, String username, String password) {
        Connection output = null;

        try {
            // output the database connection
            output = DriverManager.getConnection(String.format("jdbc:mysql://%s:%s/%s?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC", host, port, schema), username, password);
        } catch (Throwable e) {
            Log.error(MySQL.class, e);
        }

        return output;
    }
}

免责声明:请不要过分注意变量的使用snake_case(更多的是Javascript/PHP/Python/R-ish)命名约定,使用Throwable 而不是 Exception,事实上我正在构建实用程序 classes 而不是 full-fledged classes 与他们正确设置了方法、属性和其域(public、私有、受保护等)的所有内容,以及许多其他应该存在但没有存在的东西。这是(实际上)我在 Java 的第二周,我非常愿意接受改进建议,我承认这里有很多工作要做,所以请仁慈 :P

也就是说,如果我尝试这样做:

// define the package name
package com.foo.xxxxxxxx;

// import all needed resources
import com.foo.bar.helpers.Database;
import java.sql.Connection;

/**
 * application
 * @author John Doe <...>
 */
class Application {
    public static void main(String[] args) {
        Connection connection = Database.getConnection(Database.MYSQL_CLASS, "localhost", 3306, "foo_db", "john_doe_123", "this_is_not_my_real_password!");
    }
}

我明白了:

java.lang.NoSuchMethodException: java.lang.String.getConnection(java.lang.String, java.lang.Integer, java.lang.String, java.lang.String, java.lang.String)
    at java.base/java.lang.Class.getDeclaredMethod(Class.java:2475)
    at com.foo.bar.helpers.Database.getConnection(Database.java:146)
    at com.foo.xxxxxxxx.Application.main(Application.java:61)

如果我没有正确阅读文档,我需要获取我打算反映的 class 的实例,获取我想与 getDeclaredMethod 一起使用的特定方法(因为每个方法在我的任何实用程序 classes 都是静态的),方法名称为 String 和可变数量的参数(或数组,如果我正确使用它)类型为 class对于我要调用方法的每个参数。

完成后,我需要调用将 null 作为第一个参数传递的方法(因为它是静态方法,而静态方法不需要 class 的实例,我正在尝试调用特定方法)和可变数量的参数(与以前相同)以及参数本身。

我从 e.printStackTrace() 得到的那个错误告诉我方法获取失败,要么是因为我没有正确指定 class 类型(我非常怀疑使用 Class<?>[] 而不是 Class[] 但 IntelliJ 抱怨 Raw use of parameterized class 'Class') 或者我没有真正得到 class 的实例我打算从中得到一个实例并且我得到了某种通用的class 对象(所以我真的看不到我正在寻找的方法)。

或者可能是因为我声明了一个私有构造函数来避免实例化(但我认为,在阅读一些文章后,实用程序 classes(如果你真的需要使用它们)应该有一个避免实例化...因此是私有构造函数声明)但是,无论哪种方式,我现在都被搞砸了:(

这个想法是能够连接到任何给定的数据库(因为现在只有 MySQL 和 SQLite,但将来可能是 Amazon Redshift、BigQuery、PostgreSQL、Oracle 等) 但我可能以错误的方式理解了通用访问。

你能给我一个提示吗?

您提供的异常提示您试图在 class java.lang.String 中找到方法 getConnection()。我怀疑你没有把它放在那里,所以它找不到任何东西。

Database#getConnectionclass中我注意到了下面的语句

Class<?> target_class = Class.forName(parameters[0].getClass().getCanonicalName());

这基本上意味着您将第一个参数作为 class 对象处理。 (您首先获取 class 类型的实例,然后从那里获取名称)。但是在你的测试中,你给它一个 String 类型的参数。

所以这里有一些需要注意的地方。每个对象(即每个非原始值)都有一个 class 类型,甚至返回的 class 类型本身。如果不小心,这会使这变得混乱。


所以我可以在几秒钟内想到 3 种情况,您可以解决这个特定问题:

传递一个 Class 实例,例如

Database.getConnection(MySQL.class,  ... );

// in the #getConnection class
Class<?> target_class = parameters[0] // type is already a class, so just assign

传递所需 Class 类型的实例

Database.getConnection(new MySQL() ,  ... ); // defenitly not recommended, only really useable if an instance itself is needed (e.g. Non-static access) 

// in the #getConnection class
Class<?> target_class = parameters[0].getClass() // get the class-type instance

传递所需 class 类型的字符串表示形式(规范名称)

Database.getConnection(MtSQL.DB_CLASS_NAME,  ... ); // pass String type argument

// in the #getConnection class
Class<?> target_class = Class.forName(parameters[0]) // the #forName needs a String argument, so we can pass it directly.

在最新的示例中,您可以使用 ClassLoader 等。它提供了很好的功能,例如缓存和 class 卸载。但它非常复杂,所以可能不适合您的初次尝试。

最后,作为一般性建议。 Java 是强类型,具有方法重载等功能。因此,为了您自己的理智,请尝试(滥用)尽可能多地使用它。以上 3 种情况很容易被重载,从而使参数验证成为一项不那么痛苦的任务。它 'foolproof' 也为 API 用户使用它,因为在编译过程中会注意到类型不匹配。