Search code examples
javareflectionstatic-methodsvariadic-functionsutility-method

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


I built a _VERY_ rudimentary utility class in Java to handle database operations (connection retrieval, inserts, etc.) like this:

// 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;
    }
}

Apart from the database helper, I have two database connectors (MySQL and SQLite) like this (showing the MySQL connector):

// 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;
    }
}

DISCLAIMER: Please, don't pay excessive attention to things like using snake_case (which is more Javascript/PHP/Python/R-ish) naming convention for variables, using Throwable instead of Exception, the fact that I'm building utility classes instead of full-fledged classes with their methods, properties and everything with its domain (public, private, protected and the like) correctly set up and many other things that should be there and they aren't. This is (practically) my second week with Java, I'm very open for suggestions on improvement and I admit there's a lot of work to be done here and there so be benevolent :P

That said, if I try to do this:

// 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!");
    }
}

I get this:

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)

If I did read the documentation correctly, I need to get an instance of the class I intend to reflect, get the specific method I want to use with getDeclaredMethod (because every method in any of my utility classes is static) with the method name as String and a variable number of arguments (or an array, if I'm using it correctly) with the class type for each of the arguments I'm going to invoke the method with.

Done that, I need to invoke the method passing null as the first argument (because it's a static method and static methods do not need an instance of the class I'm trying to call the specific method for) and a variable number of arguments (same as before) with the parameters themselves.

That error I'm getting from e.printStackTrace() tells me the method acquisition failed, either because I didn't specified the class types correctly (I'm very doubtful about using Class<?>[] instead of Class[] but IntelliJ complains with Raw use of parameterized class 'Class') or I didn't really get an instance of the class I intend to get an instance from and I got some sort of a generic class object instead (so I can't really see the method I'm looking for).

Or maybe it's because I declared a private constructor to avoid instantiation (but I thought, after reading some articles, that utility classes (if you really need to use them) should have one to avoid instantiation... hence the private constructor declaration) but, either way, I'm a bit screwed at the moment :(

The idea is to be able to connect to any given database (because, right now, it's just MySQL and SQLite but it could be Amazon Redshift, BigQuery, PostgreSQL, Oracle, etc. in the future) but I may be getting the idea about generic access in the wrong way.

Can you give me a hint?


Solution

  • The exception you gave hinted that you tried to find the method getConnection() in the class java.lang.String. I suspect you didnt put it there, so it wouldn't find anything.

    In the Database#getConnection class I noticed the following statement

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

    This is basically means you handle the first argument as a class Object. (you first get the class-type instance, and get the name from there). But in your test, you gazve it a String-type argument.

    So here is something to be aware of. Every Object (i.e. Every non-primitive value) has a class-type, even the returned class-type itself. Which makes this confusing if not carefull.


    So there are 3 cases I could think of in a few seconds you could handle this particular problem:

    Pass a Class instance like

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

    Pass a instance of the desired Class-type like

    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
    

    Pass a String representation (canonical name) of the desired class type

    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.
    

    In the latest example you could go fancy using ClassLoader's etc.. It offers nice features like caching and class unloading. But its quite complicated, so maybe not something for your first approuch.

    Lastly, as a general advice. Java is Strong typed with features like method overloading. So try to (ab)use it as much as possible for your own sanity sake. The above 3 cases can easily be overloaded making parameter validation a less painfull task. It makes the use of it 'foolproof' for API users as well, as type-missmatching will be noticed during compiling.