How to make a java method with a dynamic number of arguments set at compile time (like a crush)

I want to make an enumeration of messages with each message that has an enumeration type, in order to avoid errors with typos in the message keys. I also want to use parameters (e.g. #{0}) to be able to insert names and more information. To make things a lot simpler, I would like to add a method getthat has a dynamic number of (string) arguments - one for each parameter that I want to replace. The exact number of arguments must be set at compile time and determined by the field of this enumeration value.

Consider this listing:

public enum Message {
    // Written by hand, ignore typos or other errors which make it not compile.

    NO_PERMISSION("no_permission", 0),
    YOU_DIED("you_died", 1),
    PLAYER_LEFT("player_left", 2);

    private String key;
    private int argAmount;

    Message(String key, int argAmount) {
        this.key = key;
        this.argAmount = argAmount;
    }

    public String replace(String... args) {
        String message = get();
        for (int i = 0; i < args.length; i++) {
            message.replace("#{" + i + "}", args[i]);
        }

        return message;        
    }

    public String get() {
        return myConfigFileWrapper.getMessage(key);
    }
}

, Message.YOU_DIED.replace(myInformation). , , YOU_DIED, , , , .

, : PLAYER_LEFT , x y. .lang player_left= The player #{0} left with the score #{1}!. Message.PLAYER_LEFT.replace(name, score). , , , 100 . , , The player #{0} left with the score #{1}! The player #{1} just left!.

, , get . , IDE , .

, varargs . , . , , , - - .

Message , get : get(String name, String score). - . (). , "" , .

API , , , , . , , ( , , ) , , , .

, , . Lombok , . , ​​ - , , .

, ? ?

.

+4
4

.

@biziclop, @bayou.io @Aasmund Eldhuset 3 , , . , , OP (). , , .

@Radiodef, , , , , maven. , maven , , Apache Maven . , maven, 2.

, , , , : maven: org.apache.velocity: speed: 1.7: jar.

, . , POM.

4 POMs:

  • RootProject
  • ActualProject
  • AnnotationProcessors

, RootProject - , , pom :

<modules>
    <module>ActualProject</module>
    <module>Annotations</module>
    <module>AnnotationProcessors</module>
</modules>

<!— Global dependencies can be configured here as well —>

, , , AnnotationProcessors. AnnotationProcessors Annotation, maven:

  • AnnotationProcessors
  • ActualProject

, , . , -proc:none:

<plugin>
     <groupId>org.apache.maven.plugins</groupId>
     <artifactId>maven-compiler-plugin</artifactId>
     <version>3.3</version>
     <configuration>
         <compilerArgs>
             <arg>-proc:none</arg>
         </compilerArgs>
     </configuration>
</plugin>

maven-processor-plugin build-helper-maven:

<plugin>
    <groupId>org.bsc.maven</groupId>
    <artifactId>maven-processor-plugin</artifactId>
    <version>2.2.4</version>
    <executions>
        <!-- Run annotation processors on src/main/java sources -->
        <execution>
            <id>process</id>
            <goals>
                <goal>process</goal>
            </goals>
            <phase>generate-sources</phase>
            <configuration>
                <outputDirectory>target/generated-sources</outputDirectory>
                <processors>
                    <processor>my.annotations.processors.MessageListProcessor</processor>
                </processors>
            </configuration>
        </execution>
    </executions>
</plugin>

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <version>1.9.1</version>
    <executions>
        <execution>
            <id>add-source</id>
            <phase>generate-sources</phase>
            <goals>
                <goal>add-source</goal>
            </goals>
            <configuration>
                <sources>
                    <source>target/generated-sources</source>
                </sources>
            </configuration>
        </execution>
    </executions>
</plugin>

, , , String , . , , , , String getKey() String[] getParams(). () :

@MessageList("my.config.file.wrapper.type")
public enum Messages implements MessageInfo {

    NO_PERMISSION("no_permission"),
    YOU_DIED("you_died",                "score"),
    PLAYER_LEFT("player_left",          "player_name", "server_name");

    private String key;
    private String[] params;

    Messages(String key, String… params) {
        this.key = key;
        this.params = params;

    @Override
    public String getKey() { return key; }

    @Override
    public String[] getParams() { return params; }

}

, AnnotationProcessor. , AbstractProcessor , , @ . @SupportedAnnotationTypes("my.annotation.type"). . , , , , , foreach. , one @MessageList . , , , . , , . ( , .)

for (Element e : roundEnv.getElementsAnnotatedWith(MessageList.class)) {
    if (!(e.getKind() == ElementKind.ENUM)) {
        raiseErrorAt(e, "Can only annotate enum types");
        continue;
    } ... }

, . : . MessageInfo :

Class<MessageInfo> messageInfoClass = (Class<MessageInfo>) Class.forName("my.annotations.MessageInfo");

, , , ClassCastException. , . , musnt , . , .properties. , , , .

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);

// The convertToPath method just returns "src/main/java/<pathWithSlashes>.java"
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(
    new File("ActualProject/" + convertToPath(element.getQualifiedName().toString())));

// The boolean here defines whether the last separator char should be cut off.
// We need to expand the class path so we might as well leave it there.
String classpath = getCurrentClasspath(false) +
    new File("Annotations/target/Annotations-version.jar").getAbsolutePath();

File outputDir = new File("ActualProject/target/classes/");
Iterable<String> arguments = Arrays.asList("-proc:none",
    "-d", outputDir.getAbsolutePath(),
    "-classpath", classpath);

boolean success = compiler.getTask(null, fileManager, null, arguments, null, compilationUnits).call();

fileManager.close();

, , , false. getCurrentClassPath:

private String getCurrentClasspath(boolean trim) {
    StringBuilder builder = new StringBuilder();
    for (URL url : ((URLClassLoader) Thread.currentThread().getContextClassLoader()).getURLs()) {
        builder.append(new File(url.getPath()));
        builder.append(System.getProperty("path.separator"));
    }
    String classpath = builder.toString();
    return trim ? classpath.substring(0, classpath.length() - 1) : classpath;
}

, , :

URL classesURL = new URL("file://" + outputDir.getAbsolutePath() + "/");
// The current class loader serves as the parent class loader for the custom one.
// Obviously, it won’t find the compiled class.
URLClassLoader customCL = URLClassLoader.newInstance(new URL[]{classesURL}, classLoader);

Class<?> annotatedClass = customCL.loadClass(element.getQualifiedName().toString());

, , :

if (!Arrays.asList(annotatedClass.getInterfaces()).contains(messageInfoClass)) {
    raiseErrorAt(element, "Can only annotate subclasses of MessageInfo");
    continue;
}

, :

MessageList annotation = element.getAnnotation(MessageList.class);
String locals = annotation.value();

// To get the package name, I used a while loop with an empty body. Does its job just fine.
Element enclosingElement = element;
while (!((enclosingElement = enclosingElement.getEnclosingElement()) instanceof PackageElement)) ;
String packageName = ((PackageElement) enclosingElement).getQualifiedName().toString();

ArrayList<Message> messages = new ArrayList<>();
for (Field field : annotatedClass.getDeclaredFields()) {
    if (!field.isEnumConstant()) continue;

    // Enum constants are static:
    Object value = field.get(null);
    MessageInfo messageInfo = messageInfoClass.cast(value);

    messages.add(new Message(field.getName(), messageInfo.getKey(), messageInfo.getParams()));
}

Message - getter. , , . ! Velocity . - . , 3 , , ...

#set ($doubleq = '"')
#set ($opencb = "{")
#set ($closecb = "}")
package $package;

foreach:

/**
 * This class was generated by the Annotation Processor for the project ActualProject.
 */
public abstract class Message {

#foreach ($message in $messages)

#set ($args = "")
#set ($replaces = "")
#foreach ($param in $message.params)
#set ($args = "${args}String $param, ")
#set ($replaces = "${replaces}.replace($doubleq$opencb$param$closecb$doubleq, $param)")
#end
#set ($endIndex = $args.length() - 2)
#if ($endIndex < 0)
#set ($endIndex = 0)
#end
#set ($args = $args.substring(0, $endIndex))
    public static final String ${message.name}($args) {
        return locals.getMessage("$message.key")$replaces;
    }

#end

    private static final $locals locals = ${locals}.getInstance();
}

Velocity , . , , . , ? . :

  • String, args
  • :
    • "String" args.
    • ".replace(" {} ",)" params.
  • args. ( , endIndex . , endIndex 0.)
  • , 2 3.
    • , , .

Locals. , , , , . , .

, raiseErrorAt (Element, String), , , , processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg, element);

, . . , , . . - , .

0

, :

public class Message {
    public static final Message0Args NO_PERMISSION = new Message0Args("no_permission");
    public static final Message1Arg YOU_DIED = new Message1Arg("you_died");
    public static final Message2Args PLAYER_LEFT = new Message2Args("player_left");

    private String key;
    private int argAmount;

    protected Message(String key, int argAmount) {
        this.key = key;
        this.argAmount = argAmount;
    }

    // Same replace() method, but make it protected
}

, :

public class Message2Args extends Message {
    public Message2Args(String key) {
        super(key, 2);
    }

    public String replace(String first, String second) {
        return super.replace(first, second);
    }   
}

, Message enum, ( , ), enum , public static final.

+4

, , , . Message.PLAYER_LEFT.replace(name, score) Message.PLAYER_LEFT.replace(score, name)? Message.PLAYER_LEFT.replace(name, lastLocation)?

, - :

public abstract class Message<T> {

    public static final Message<Void> YOU_DIED = new Message<Void>("You died.") {
        @Override
        public String create(Void arguments) {
            return this.replace();
        }
    };

    public static final Message<Player> PLAYER_LEFT = new Message<Player>("Player %s left with score %d") {
        @Override
        public String create(Player arguments) {
            return this.replace( arguments.getName(), arguments.getScore());
        }
    };

    private Message(String template) {
        this.template = template;
    }

    private final String template;

    protected String replace( Object ... arguments) {
        return String.format( template, arguments );
    }

    public abstract String create(T arguments);
}

, , :

  • .
  • (, ) , , , . , String Message.PLAYER_LEFT, Player, .
  • , , , ? , , , .

The big disadvantage is that if you have complex messages (for example, Message.PLAYER_HITwhich must accept two type parameters Player), you need to write wrapper classes for the parameters (in our example, which encapsulate both players). It can be quite tiring.

+2
source

Personally, I would approach the problem this way, since I'm a strong guy

public interface Message
{

    public static final Message instance = loadInstance();

    String you_died(Player player);

    String player_left(Player player, int score); 

    // etc. hundreds of them
}

// usage
String x = Message.instance.player_left(player, 10);

// one subclass per language
public class Message_jp implements Message
{
    public String you_died(Player player){ return player.lastName + "君,你地死啦死啦"; }
                                           // or whatever way you like to create a String
    // etc.
}

At run time, you need to load the appropriate subclass Message.

static Message loadInstance()
{
    String lang = conf.get("language"); // e.g. "jp"
    Class clazz = Class.forName("Message_"+lang);  // Message_jp.class
    return clazz.newInstance();
}

This approach includes all messages in class files, which should be fine.

+2
source

All Articles