简体   繁体   中英

Detect constructors that "should not" be used with reflection

I have the following function:

@SuppressWarnings("unchecked")
public static <T> T createInstance(String className, Object... args) {
    try {
        Class<?> clazz = Class.forName(className);
        Class<?>[] parameterTypes = Arrays.stream(args).map(Object::getClass).toArray(Class[]::new);
        return (T) clazz.getDeclaredConstructor(parameterTypes).newInstance(args);
    } catch (Exception e) {
        throw new RuntimeException("Error", e);
    }
}

The function is supposed to get a class name (as String ), load it and, if a constructor which matches the Object... args is found, create a new instance of such object.

This code works but it may get invoked on @HotSpotIntrinsicCandidate methods. For example, if the caller does this:

String str = createInstance("java.lang.String", "Some value");

... then the function would invoke this constructor of the class java.lang.String :

@HotSpotIntrinsicCandidate
public String(String original)

My understanding of @HotSpotIntrinsicCandidate constructors is that the JVM will do a special processing for those, that's for example the reason why I'm able to do String str = "some string"; but I'm not able to do MyObject myObj = "some string" (assuming that MyObject class has a constructor which takes a String in parameter).

At the same time, I kind of understand that these constructors are "unreliable" as they strictly depend on what the JVM guys decide. For example, the code would work as well with boxed primitives such as Integer :

Integer myInt = createInstance("java.lang.Integer", "3");

... but these constructors have been deprecated since Java 9 (and the new @HotSpotIntrinsicCandidate s of these classes are the valueOf(...) methods instead which are not constructors anymore).

My feeling is that I should reject any request on constructors that are either @Deprecated or @HotSpotIntrinsicCandidate in order to avoid callers building blocks on something that may potentially get deprecated one day.

  • Can anyone confirm (or deny) my assumption?
  • Assuming that my intuition is correct, I can easily getAnnotation(Deprecated.class) on the Constructor that I retrieve (since this annotation is public) but I cannot getAnnotation(HotSpotIntrinsicCandidate.class) since this annotation is package-private to jdk.internal . If that was the way, how can I detect all the classes that I should reject from building?

My understanding of @HotSpotIntrinsicCandidate constructors is

Completely wrong.

HotSpot refers to the notion that the JVM will run all java code extremely stupidly/slowly, even slower than what you imagine, because it also does a boatload of seemingly pointless bookkeeping (how often does this 'if' branch one way vs. the other way - let's just count it for no reason).

... because with all that bookkeeping, the JVM can easily identify the 1% of the code that is eating 99% of the resources, and will then take quite a bit of time (and using all that bookkeeping it is doing) to generate some finely tuned machine code, specialized for the exact hardware you run on and optimized to run most quickly for the cases observed so far (that's the bookkeeping). And now you have extremely fast code instead. And given that this 1% of the code is actually eating 99% of the resources, that's why JVMs are actually really fast. That process of 'rewriting', that's hotspot. It's purely a Java-The-Virtual-Machine thing. Java-The-Language doesn't have hotspot, has no idea what it is, and you're talking about a language feature, ergo - no, that's not what @HotspotIntrinsicCandidate means, at all.

The reason you can write String x = "hello" is because the java language specification dictates this. That's it. That's the only reason. Because it's hardcoded in the spec. (Same for why you can write Integer i = 5; - because '5' is written down as a concept in the JLS, and 'hey if the code doesn't compile but turning a primitive into its matching wrapper type would make it work - then assume the programmer meant to do that and compile it as if it said Integer.valueOf(5) instead' - that is called 'autoboxing' and is also written in the JLS, which makes that work. Again, @HotspotIntrinsicCandidate has nothing to do with it).

@HIC refers to the idea that a JVM implementation is free to have a "pre-written" finely tuned machine code version of it, available straight within the JVM implementation itself, and the JVM runner should look for, and then use such a thing if it is available. It is no guarantee (but most JVM implementations do work this way; there's not all that much point doing bookkeeping on 'how often is jlString 's hashCode() method invoked' - anytime you shove strings in hashmaps means it'll get invoked, a lot. Might as well 'precompile' the machine code for it and inject it straight into java.exe . Why wait, right?

Given that it's purely an optimization move, presence or lack thereof has absolutely not one iota of effect on whether 'it is a good idea' to expose that constructor or not.

My feeling is that I should reject any request on constructors that are either @Deprecated

Deprecation is an odd beast. It's used for many different things. There's no particular point in not using such constructors - sure, they might disappear in the future, but there are other reasons for Deprecation markers (sometimes it's just "This method is a bad idea, in the sense that most who use it are confused". For example, juDate 's getYear() method is deprecated. I'll gladly take a bet at 1000 to 1 odds that it'll still exist 10 years from now. It won't be going anywhere. It's just a bad idea to use.

Which gets us to...

This is a bad idea, period.

Class<?>[] parameterTypes = Arrays.stream(args).map(Object::getClass).toArray(Class[]::new);

This doesn't work.

Trivial example:

class Example {
  public Example(Number n) {
    System.out.println("Hello!");
  }
}

...

createInstance("com.foo.Example", 5);

The above does not work!!

That's because invoking .getClass() on that 5 gets you java.lang.Integer.class . This is entirely 'compatible' with Number (it's a subclass of it) - new Example(5) works just fine. However, getConstructor() searches for exactly what you ask for, which is a constructor that takes 1 Integer . That's not there. You have a constructor that takes a supertype of Integer.

The one and only way to fix this is to use .getDeclaredConstructors() , go through all of them, and use .isSubclass and friends on each and every argument, also adding code to deal with varargs invokes if you like, and then using that.

This is incredibly slow .

More generally this approach means you take the worst aspects of laissez-faire compiler-less languages (usually called 'scripting languages'), and the worst aspects of java, whilst leaving the best parts out. What possible point does this serve?

I bet you're looking for some of these options:

Build and compiler infra is much better than you think it is

There are many tools out there that are extremely smart about knowing what java code needs to be compiled (ie only the thing you changed), and that compiles just those things. For example, eclipse (the editor) will just automatically and near instantaneously keep all your stuff compiled, continuously. If this is an attempt to avoid the compile-build loop, there's no need for that.

You can even run java foo.java and the JVM will automatically compile and run it, for trivial one-file projects.

There are java-esque scripting languages

You can just run javascript in java if you want. The GraalVM project has a java library that just runs javascript. Why not use that? If you'd like the syntax to be specifically java-like, there's beanshell and groovy.

There are module systems and dynamic loaders

You can set up, in plain jane java (look up the ClassLoader class in the javadocs, or the OSGi project, for example), a system that can 'live-reload' classes in a running JVM. If the aim is that you can start a JVM, and then manually 'program in it' as it runs, you can use those.

Debuggers are awesome

Debuggers can plug into running JVMs, even across an inte.net connection if you want, and just run code you type, on demand, in the middle of a breakpointed thread. In eclipse, for example, just set a breakpoint anywhere, run the thing (click the 'bug' button, that runs it in debug mode), and then just open the debug shell and type whatever you want. You can even access the local variables, they'll have whatever value they have when the breakpoint was hit. You can do this to 'live code' running on a server if you like.

There's JSP

I strongly, strongly advise against this, but JSP is a thing where you can stick java code amongst your HTML in a .jsp file and have a web server just run this. It'll take care of everything - detecting that it changed, (re)compiling it, and running it. You can trivially edit the JSP file and just reload a page and now you're looking at the new stuff.

Some require you to tell it: Yes, please check for changes and re-compile.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM