【问题标题】:Java trait workaroundJava trait 解决方法
【发布时间】:2021-03-23 13:12:02
【问题描述】:

我正在使用接口中的新默认函数,并且需要一种方法来为一组类提供类似 trait 的行为。

我想出的是这样的:

public interface Resource {

    Map<Resource, AnalysisTag<?>> tags = new WeakHashMap<>();

    @JsonIgnore
    default AnalysisTag<?> getTag() {
        return tags.get(this);
    }

    default Resource tag(AnalysisTag<?> tag) {
        tags.put(this, tag);
        return this;
    }
}

在这里,Resource 接口定义了实现类的行为,以便能够存储与特定对象关联的标签。当对象在代码的其他部分丢失所有引用时,WeakHashMap 会自动清理它。

WeakHashMaps 不会阻塞其键的 GC。

我的问题是,这安全吗?有什么需要注意的吗?

【问题讨论】:

    标签: java traits mixins


    【解决方案1】:

    我的问题是,这样安全吗?

    AFAIK ...是的...在某种意义上它不应该导致内存泄漏。

    有什么需要注意的吗?

    一个问题是WeakHashMap 使用WeakReferenceWeakReference 对于垃圾收集器来说处理起来相对昂贵。引用队列在 GC 本身运行后处理,许多 JVM 的默认设置是只有一个线程来处理它们。这可能会减慢(在这种情况下)WeakHashMap 条目的回收速度,这可能会对性能产生连锁反应。

    我花了一些时间来弄清楚这应该如何工作。 (我忘了tags 隐含地是static final,因为它是在接口中声明的。Duh!)我认为它可能会作为“mixin”来为类添加“具有标签”特征。然而,这是一种昂贵的方式。在实现Resource 的类中添加private Tag&lt;?&gt; tag 字段+ getter 和setter 会更有效。

    如果将tags 重命名为TAGS,代码会更容易理解(对于关注标识符命名提示的老派Java 程序员)。

    【讨论】:

    • 对一个长期运行的程序会有多少惩罚?
    • 嗯...这取决于Resource 实例的创建频率和垃圾收集频率,以及标记操作的调用频率。诸如此类。
    • 它们会被非常频繁地创建,它们基本上是我的应用程序的 DTO。它们也将很快被取消引用,因为它们不需要长时间留在内存中。
    • 那么,您需要考虑 GC 性能影响。
    【解决方案2】:

    好的...我看到了您的解决方案的两个问题:

    1. tags 的访问不安全,任何其他类都可以操纵它。更多内容如下。
    2. 就速度而言,检索不是最佳的。然而,这在 Java 社区中被广泛接受,因为像 ThreadLocal 这样的可憎之物也存在。所以让我们接受这一点。

    因此,您需要做的是保护地图免受恶意访问。 我已经构建了三个增加安全考虑的示例。

    示例包结构:

    stackoverflow.trait0r/
        AnalysisTag.java
        Trait0r.java
        /bad/
            BadResource.java
        /better/
            BetterResource.java
            BetterResourceContainer.java
        /best/
            BestResource.java
            BestResourceContainer.java
            SamePackageIllegalAccessTest.java
    

    示例 1:您的,只是改进了一点

    package stackoverflow.trait0r.bad;
    
    import java.util.WeakHashMap;
    
    import stackoverflow.trait0r.AnalysisTag;
    
    public interface BadResource {
        /* best to always use full type (WeakHashMap instead of Map) when in control of code.
         * so downcast is always possible and upcast never necessary.
         * Plus access to the references object is usually faster
         */
        WeakHashMap<BadResource, AnalysisTag<?>> tags = new WeakHashMap<>();
        default AnalysisTag<?> getTag() {
            return tags.get(this);
        }
        default BadResource tag(final AnalysisTag<?> tag) {
            tags.put(this, tag);
            return this;
        }
    }
    

    示例 2:更好一点,使用受保护的包

    package stackoverflow.trait0r.better;
    
    import stackoverflow.trait0r.AnalysisTag;
    
    public interface BetterResource {
        default AnalysisTag<?> getTag() {
            return BetterResourceConainer.tags.get(this);
        }
        default BetterResource tag(final AnalysisTag<?> tag) {
            BetterResourceConainer.tags.put(this, tag);
            return this;
        }
    }
    

    package stackoverflow.trait0r.better;
    
    import java.util.WeakHashMap;
    
    import stackoverflow.trait0r.AnalysisTag;
    
    class BetterResourceConainer {
        static /* package */ WeakHashMap<BetterResource, AnalysisTag<?>> tags = new WeakHashMap<>();
    }
    

    示例 3:最佳保护:包保护和线程访问检查

    package stackoverflow.trait0r.best;
    
    import stackoverflow.trait0r.AnalysisTag;
    
    public interface BestResource {
        default AnalysisTag<?> getTag() {
            return BestResourceConainer.getTag(this);
        }
        default BestResource tag(final AnalysisTag<?> tag) {
            BestResourceConainer.setTag(this, tag);
            return this;
        }
    }
    

    package stackoverflow.trait0r.best;
    
    import java.util.WeakHashMap;
    
    import stackoverflow.trait0r.AnalysisTag;
    
    class BestResourceConainer {
        static private WeakHashMap<BestResource, AnalysisTag<?>> tags = new WeakHashMap<>();
        static private void ensureAccess() {
            final int CALLER_INDEX = 3;
            final StackTraceElement[] st = Thread.currentThread().getStackTrace();
            if (st.length < CALLER_INDEX + 1) throw new IllegalAccessError("Cannot access BestResourceConainer via external caller!");
    
            final StackTraceElement ste = st[CALLER_INDEX];
            final boolean grantAccess = BestResource.class.getName().equals(ste.getClassName());
            if (!grantAccess) throw new IllegalAccessError("Cannot access BestResourceConainer via external caller!");
        }
        static /* package */ void setTag(final BestResource pRes, final AnalysisTag<?> pTag) {
            ensureAccess();
            tags.put(pRes, pTag);
        }
        static AnalysisTag<?> getTag(final BestResource pRes) {
            ensureAccess();
            return tags.get(pRes);
        }
    }
    

    以及一次肮脏的小黑客尝试:

    package stackoverflow.trait0r.best;
    
    import stackoverflow.trait0r.AnalysisTag;
    
    public class SamePackageIllegalAccessTest {
    
        public static void main(final String[] args) {
            try {
                BestResourceConainer.setTag(new BestResource() {/**/}, new AnalysisTag<>("[ malicious hack ]"));
                System.out.println("Write successful!");
            } catch (final Error e) {
                System.out.println("Write access failed as expected");
            }
    
            try {
                final AnalysisTag<?> tag = BestResourceConainer.getTag(new BestResource() {/**/});
                System.out.println("TAG: " + tag);
    //                      ThreadLocal<String> x;
            } catch (final Error e) {
                System.out.println("Read access failed as expected");
            }
        }
    
    }
    

    如您所见,如果您真的想要对tags 映射进行某种安全管理,有一些技术可以实现。

    但是请注意,您可能必须调整 CALLER_INDEX,如果从一开始就将 BestResource 类替换为相同类名的另一个实现,这仍然可能被欺骗。

    【讨论】:

    • 是的,我知道这个问题,我认为为标签映射使用包私有包装类就足够了。
    【解决方案3】:

    通常这很好,但一旦您开始使用字段成员,我会考虑使用抽象类并将您的地图设为私有/最终地图。

    查看以下内容以获取更多信息...

    https://softwareengineering.stackexchange.com/questions/184945/why-is-an-interface-in-java-not-allowed-to-have-state

    https://www.baeldung.com/java-weakhashmap

    【讨论】:

    • 使用抽象类的问题是不能多重继承,记录不能扩展类。
    • 这是真的,但通常意味着设计存在问题。而不是让多个类继承您的接口,您应该有一个提供给多个类的实现。所以你有另一个基类(抽象类),它通过构造函数(或需要的方法参数)接受你的接口。这将使单元测试更容易,因为您只单独测试该接口,然后您的其他类不再关心测试期间的实现,因为您将提供一个模拟。这是为了让你的课程简单
    猜你喜欢
    • 1970-01-01
    • 2017-09-03
    • 1970-01-01
    • 1970-01-01
    • 2010-12-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多