如何在没有引用的情况下找到类的单例对象?

huangapple go评论73阅读模式
英文:

How to find singleton object of class without having reference to it?

问题

考虑以下示例:

import java.util.Map;
import java.util.Optional;

class Scratch {

    public static class UserService {

        private final Map<String, String> users = Map.of(
                "user1", "Max",
                "user2", "Ivan",
                "user3", "Leo");

        public Optional<String> findUserById(String userId) {
            return Optional.ofNullable(users.get(userId));
        }
    }

    // 我们将单例对象保存在这个字段中
    // 以防止被垃圾回收
    private volatile UserService userService = null;

    private UserService getUserService() {
        // TODO 我们需要实现它
        // 显然,我们不能返回 Scratch#userService 字段
        return null;
    }

    public void doJobs() {
        // 我需要在这里获取 `userService`,而不需要显式传递它
        // 来执行某些服务方法

        UserService userService = getUserService();
        if (userService != null) {
            userService.findUserById("userId");
        }
    }

    public void startApplication() {
        userService = new UserService();

        doJobs();
    }

    public static void main(String[] args) {
        Scratch program = new Scratch();
        program.startApplication();
    }
}

所以,我们有一个简单的Java应用程序,没有使用任何像Spring之类的框架。我需要在 doJobs() 方法中找到 UserService 对象,而不需要显式传递它。显然,这是一个面试题。

以下是任务的前提条件:

  • UserService 不是Spring bean或类似的东西。与“依赖注入(DI)”无关。
  • 不能显式地将 UserService 对象传递给 doJobs() 方法。
  • 不能将 UserService 对象设置为某个静态/全局变量、接口或方法。
  • 不能使用Java代理(javaagents)。
  • 你知道在当前类加载器中只有一个 UserService 对象。
  • 如果愿意,可以使用任何反射(包括库)。
  • 不能创建新对象,必须使用已存在的对象。
  • 不能将 Scratch#userService 字段用于任何目的。它是为了防止垃圾回收而引入的。

因此,一般来说,我们需要以某种方式获取所有对象的列表,并使用有关类名的知识找到所需的对象。

在面试时,我没有解决这个任务。你能帮我做一下吗?

英文:

Let's consider the following example:

import java.util.Map;
import java.util.Optional;

class Scratch {

    public static class UserService {

        private final Map&lt;String, String&gt; users = Map.of(
                &quot;user1&quot;, &quot;Max&quot;,
                &quot;user2&quot;, &quot;Ivan&quot;,
                &quot;user3&quot;, &quot;Leo&quot;);

        public Optional&lt;String&gt; findUserById(String userId) {
            return Optional.ofNullable(users.get(userId));
        }
    }

    // We save our singleton object to this filed
    // to protect from being garbage collected
    private volatile UserService userService = null;

    private UserService getUserService() {
        /// TODO we need to implement it
        /// Obviously, we cannot return Scratch#userService field
        return null;
    }

    public void doJobs() {
        // I need to get `userService` here, without explicitly passing it
        // to execute some service method

        UserService userService = getUserService();
        if (userService != null) {
            userService.findUserById(&quot;userId&quot;);
        }
    }

    public void startApplication() {
        userService = new UserService();

        doJobs();
    }

    public static void main(String[] args) {
        Scratch program = new Scratch();
        program.startApplication();
    }
}

So. we have simple java application without any frameworks, like spring. I need to find UserService object in doJobs() method, without explicitly passing it. Obviously, it is job interview question.

There are the following task preconditions:

  • UserService is not a spring bean or something like this. It is not about DI
  • You cannot explicitly pass UserService object to doJobs() method
  • You cannot set UserService object to some static/global variable/interface/method.
  • You cannot use javaagents.
  • You know, that there is only one object of UserService in current class loader.
  • You may use any reflection (included libraries), if you wish
  • You cannot create new object, you should use existed one
  • You cannot use Scratch#userService field for any purpose. It is introduced for protection from gc.

So, generally speaking we need to get somehow list of all objects and find needed one, using knowledge about Class name.

I did not solve this task on job interview. Could you help me to do it?

答案1

得分: 2

或许你忽略了一些问题细节,其重要性你并没有意识到。例如,以下示例适用于HotSpot/OpenJDK和衍生的JREs:

import java.lang.ref.Reference;
import java.lang.reflect.*;
import java.util.*;

class Scratch {
    public static class UserService {
        private final Map<String, String> users = Map.of(
            "user1", "Max", "user2", "Ivan", "user3", "Leo");
        public Optional<String> findUserById(String userId) {
            return Optional.ofNullable(users.get(userId));
        }
        @Override
        protected void finalize() throws Throwable {
            System.out.println("everything went wrong");
        }
    }
    private volatile UserService userService; // never read

    private UserService getUserService() {
        try {
            Class<?> c = Class.forName("java.lang.ref.Finalizer");
            Field[] f={c.getDeclaredField("unfinalized"), c.getDeclaredField("next")};
            AccessibleObject.setAccessible(f, true);
            Reference r = (Reference)f[0].get(null);
            while(r != null) {
                Object o = r.get();
                if(o instanceof UserService) return (UserService)o;
                r = (Reference)f[1].get(r);
            }
        } catch(ReflectiveOperationException ex) {}
        throw new IllegalStateException("was never guaranteed to work anyway");
    }

    public void doJobs() {
        UserService userService = getUserService();
        System.out.println(userService);
        userService.findUserById("userId");
    }

    public void startApplication() {
        userService = new UserService();
        doJobs();
    }
    public static void main(String[] args) {
        Scratch program = new Scratch();
        program.startApplication();
    }
}
Scratch$UserService@9807454

关键的一点是,拥有非平凡的 finalize() 方法会导致创建一个特殊引用,允许在没有对该对象的其他引用存在时执行终结操作。上述代码遍历了这些特殊引用。

这也提供了一个线索,解释为什么不存在其他解决方案(无需读取字段)。如果字段包含了指向对象的唯一引用,那么只有这个引用才能区分实际的、存在的对象和例如仅仅因为偶然情况下包含相同位模式的内存块,或者垃圾,即曾经是对象的内存块,但现在与从未包含对象的内存没有区别。

垃圾收集器不关心未使用的内存,无论它以前是否包含对象,它们遍历活动引用以确定可达对象。因此,即使您找到了一种在垃圾收集器发现现有 UserService 实例时可以利用其内部的方法,您也只是间接地读取了 Scratch.userService 字段,因为垃圾收集器会执行这个操作,以发现该对象的存在。

唯一的例外是终结操作,因为它将在没有对该对象的其他引用存在时有效地使对象复活,从而需要特殊引用,上述代码就是利用了这个。这个附加引用是在构造 UserService 实例时创建的,这也是积极使用终结操作会使内存管理效率降低的原因之一,因此也参见 https://stackoverflow.com/q/41289671/2711488https://stackoverflow.com/q/41378606/2711488


话虽如此,我们必须澄清另一点:

在这种特定情况下,字段 userService 不会阻止垃圾收集

这可能与直觉相矛盾,但正如在 https://stackoverflow.com/q/24376768/2711488 中所详述的那样,由局部变量引用的对象本身并不会阻止垃圾收集。如果变量随后未被使用,引用的对象可能会被垃圾收集,但语言规范甚至明确允许代码优化以减少可达性,这可能会导致出现诸如 [这个]、[那个] 或 [另一个] 的问题。

在这个示例中,Scratch 实例仅由局部变量引用,并且在将引用写入 userService 字段之后,完全未被使用,即使没有运行时优化。甚至要求不读取该字段,换句话说,不使用。因此,原则上,Scratch 实例有资格进行垃圾收集。注意,由于 Scratch 实例的局部性质,volatile 修饰符没有意义。即使对象不是纯局部的,没有任何读取使其变得毫无意义,尽管优化器很难识别。因此,由于 Scratch 实例有资格进行垃圾收集,只有由可回收对象引用的 UserService 实例也具有相同资格。

上述示例之所以仍然有效,是因为它的运行时间不足以触发运行时代码优化或垃圾收集。但是,重要的是要理解,在没有字段的情况下,对象是否在内存中持续存在是没有保证的,因此假设在堆内存中一定有一种方法来找到它是错误的。

英文:

Maybe you missed some question details whose importance you did not realize. E.g., the following example works with HotSpot/OpenJDK and derived JREs:

import java.lang.ref.Reference;
import java.lang.reflect.*;
import java.util.*;

class Scratch {
    public static class UserService {
        private final Map&lt;String, String&gt; users = Map.of(
            &quot;user1&quot;, &quot;Max&quot;, &quot;user2&quot;, &quot;Ivan&quot;, &quot;user3&quot;, &quot;Leo&quot;);
        public Optional&lt;String&gt; findUserById(String userId) {
            return Optional.ofNullable(users.get(userId));
        }
        @Override
        protected void finalize() throws Throwable {
            System.out.println(&quot;everything went wrong&quot;);
        }
    }
    private volatile UserService userService; // never read

    private UserService getUserService() {
        try {
            Class&lt;?&gt; c = Class.forName(&quot;java.lang.ref.Finalizer&quot;);
            Field[] f={c.getDeclaredField(&quot;unfinalized&quot;), c.getDeclaredField(&quot;next&quot;)};
            AccessibleObject.setAccessible(f, true);
            Reference r = (Reference)f[0].get(null);
            while(r != null) {
                Object o = r.get();
                if(o instanceof UserService) return (UserService)o;
                r = (Reference)f[1].get(r);
            }
        } catch(ReflectiveOperationException ex) {}
        throw new IllegalStateException(&quot;was never guaranteed to work anyway&quot;);
    }

    public void doJobs() {
        UserService userService = getUserService();
        System.out.println(userService);
        userService.findUserById(&quot;userId&quot;);
    }

    public void startApplication() {
        userService = new UserService();
        doJobs();
    }
    public static void main(String[] args) {
        Scratch program = new Scratch();
        program.startApplication();
    }
}
Scratch$UserService@9807454

The crucial aspect is that having a nontrivial finalize() method causes the creation of a special reference that allows to perform the finalization when no other reference to the object exists. The code above traverses these special references.

This does also provide a hint why no other solution (without reading the field) can exist. If the field contains the only reference to the object, only this reference makes the difference between an actual, existing object and, e.g. a chunk of memory that just happens to contain the same bitpattern by chance. Or garbage, i.e. a chunk of memory that happened to be an object in the past, but now is not different to memory that never contained an object.

Garbage collectors do not care about the unused memory, whether it contained objects in the past or not, they traverse the live references to determine the reachable objects. So even if you found a way to peek into the internals to piggyback on the garbage collector when it discovers the existing UserService instance, you just read the field Scratch.userService indirectly, as that’s what the garbage collector will do, to discover the existence of that object.

The only exception is finalization, as it will effectively resurrect the object to invoke the finalize() method when no other reference to it exists, which requires the special reference, the code above exploited. This additional reference has been created when the UserService instance was constructed, which is one of the reasons why actively using finalization makes the memory management less effecient, so also https://stackoverflow.com/q/41289671/2711488 and https://stackoverflow.com/q/41378606/2711488


That said, we have to clarify another point:

In this particular scenario, the field userService does not prevent garbage collection.

This may contradict intuition, but as elaborated in https://stackoverflow.com/q/24376768/2711488, having an object referenced by a local variable does not prevent garbage collection per se. If the variable is not subsequently used, the referenced object may get garbage collected, but the language specification even explicitly allows code optimization to reduce the reachability, which may lead to issues like this, that, or yet another.

In the example, the Scratch instance is only referenced by local variables and, after writing the reference to the userService field, entirely unused, even without runtime optimizations. It’s even a requirement that the field is not read, in other words, unused. So in principle, the Scratch instance is eligible to garbage collection. Note that the due to the local nature of the Scratch instance, the volatile modifier has no meaning. Even if the object was not purely local, the absence of any read made it meaningless, though this is hard to recognize by optimizers. So, since the Scratch instance is eligible to garbage collection, the UserService instance only referenced by the collectible object is too.

The above example still works because it doesn’t run long enough to make runtime code optimizations or garbage collection happen. But it’s important to understand that there is no guaranty that the object persists in memory, even with the field, so the assumption that there must be a way to find it in heap memory, is wrong in general.

答案2

得分: 1

public static class UserService {

    private final Map<String, String> users = Map.of(
        "user1", "Max",
        "user2", "Ivan",
        "user3", "Leo");

    public Optional<String> findUserById(String userId) {
        return Optional.ofNullable(users.get(userId));
    }
}

// 我们将单例对象保存到此字段中,
// 以防止被垃圾回收
private volatile UserService userService = new UserService();

private UserService getUserService() {
    // TODO 我们需要实现它
    // 显然,我们不能返回 Scratch#userService 字段
    return userService;
}

public void doJobs() {
    // 在这里获取 `userService`,而无需显式传递它
    // 来执行某些服务方法

    UserService userService = getUserService();
    if (userService != null) {
        System.out.println(userService.findUserById("user1"));
    }
}

public void startApplication() {
    userService = new UserService();

    doJobs();
}

public static void main(String[] args) {
    Scratch program = new Scratch();
    program.startApplication();
}
英文:
public static class UserService {
private final Map&lt;String, String&gt; users = Map.of(
&quot;user1&quot;, &quot;Max&quot;,
&quot;user2&quot;, &quot;Ivan&quot;,
&quot;user3&quot;, &quot;Leo&quot;);
public Optional&lt;String&gt; findUserById(String userId) {
return Optional.ofNullable(users.get(userId));
}
}
// We save our singleton object to this filed
// to protect from being garbage collected
private volatile UserService userService = new UserService();
private UserService getUserService() {
/// TODO we need to implement it
/// Obviously, we cannot return Scratch#userService field
return userService;
}
public void doJobs() {
// I need to get `userService` here, without explicitly passing it
// to execute some service method
UserService userService = getUserService();
if (userService != null) {
System.out.println(userService.findUserById(&quot;user1&quot;));
}
}
public void startApplication() {
userService = new UserService();
doJobs();
}
public static void main(String[] args) {
Scratch program = new Scratch();
program.startApplication();
}

答案3

得分: 1

这个任务要求你在不直接引用该字段的情况下实现这个方法。

private UserService getUserService() {
    return ...;
}

作为实例方法,你可以通过关键字 this 获得对 Scratch 实例的引用。使用反射,你可以获得对 userService 字段的引用,然后使用该字段来获取 this 实例的该字段的值:

Field field = getClass().getField("userService");
UserService userService = (UserService)field.get(this);

完整的方法如下:

private UserService getUserService() {
    try {
        return (UserService)getClass().getField("userService").get(this);
    } catch (ReflectiveOperationException e ) {
        throw new RuntimeException(e);
    }
}
英文:

The task asks for you to implement this method without referring to the field directly.

private UserService getUserService() {
return ...;
}

Being an instance method you have a reference to the instance of Scratch via the this keyword. Using reflection, you can get a reference to the userService field, then use the field to get the value of that field for this:

Field field = getClass().getField(&quot;userService&quot;);
UserService userService = (UserService)field.get(this);

The complete method is then:

private UserService getUserService() {
try {
return (UserService)getClass().getField(&quot;userService&quot;).get(this);
} catch (ReflectiveOperationException e ) {
throw new RuntimeException(e);
}
}

答案4

得分: 1

如果已知在静态字段中保存了UserService的一个实例,则可以尝试使用Reflections

private UserService getUserService() {
    Reflections reflections = new Reflections("", new SubTypesScanner(false));
    UserService userService = null;
    for (String name: reflections.getAllTypes()) {
        try {
            for (Field field: Class.forName(name).getDeclaredFields()) {
                if (field.getType().equals(UserService.class)) {
                    userService = (UserService) field.get(null);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    return userService;
}

如果字段不是静态的,则需要创建声明类的实例(并将其提供给null)。

private UserService getUserService() {
    Reflections reflections = new Reflections("", new SubTypesScanner(false));
    UserService userService = null;
    for (String name: reflections.getAllTypes()) {
        try {
            for (Field field: Class.forName(name).getDeclaredFields()) {
                if (field.getType().equals(UserService.class)) {
                    Object obj = (Modifier.isStatic(field.getModifiers())) ? null : field.getDeclaringClass().newInstance();
                    userService = (UserService) field.get(obj);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    return userService;
}
英文:

If it's known that an instance of UserService is saved in a static field you can try Reflections

private UserService getUserService() {
Reflections reflections = new Reflections(&quot;&quot;, new SubTypesScanner(false));
UserService userService = null;
for (String name: reflections.getAllTypes()) {
try {
for (Field field: Class.forName(name).getDeclaredFields()) {
if (field.getType().equals(UserService.class)) {
userService = (UserService) field.get(null);
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return userService;
}

If the field is not static then you'll have to create an instance of declaring class (and feed it instead of null)

private UserService getUserService() {
Reflections reflections = new Reflections(&quot;&quot;, new SubTypesScanner(false));
UserService userService = null;
for (String name: reflections.getAllTypes()) {
try {
for (Field field: Class.forName(name).getDeclaredFields()) {
if (field.getType().equals(UserService.class)) {
Object obj = (Modifier.isStatic(field.getModifiers())) ? null : field.getDeclaringClass().newInstance();
userService = (UserService) field.get(obj);
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return userService;
}

huangapple
  • 本文由 发表于 2020年9月12日 05:13:57
  • 转载请务必保留本文链接:https://go.coder-hub.com/63854380.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定