如何防止特定类调用公共方法

20

我有一个现有的类,想要向其中添加一个方法。但是我希望该方法仅从特定类的特定方法中调用。是否有任何方法可以防止其他类/方法调用该方法?

例如,我有一个现有的A类。

public final class A
{
    //other stuff available for all classes/methods

    //I want to add a method that does its job only if called from a specific method of a class, for example:

    public void method()
    {
        //proceed if called from Class B.anotherMethod() else throw Exception
    }
}

一种方法是在method()内获取StackTrace,然后确认父方法。

我想要的是一种更干净、更可取的解决方案,比如设计模式之类的东西。


你可以在类B中尝试使用method(thisobject),并通过参数扩展类A中的函数,让它检查thisobject是否为B。 - user657496
3
StackTrace 可以解决问题,但它是一个运行时解决设计时问题的方案。 - CodeFusionMobile
11个回答

10

说实话,你已经让自己陷入了困境。

如果类A和B既不相关又不属于同一个包,则可见性无法解决问题。(即使它可以,反射也可以用来规避可见性规则。)

如果代码可以使用反射调用方法,则静态代码分析也无法解决该问题。

B.this作为额外参数传递并检查其是否符合A.method(...)是无助的,因为某个其他类C也可以传递一个B实例。

这只留下了堆栈跟踪的方法1...或放弃并依赖程序员的良好意识2,不去调用他们不应该调用的方法。


理想的解决方案是重新审查导致出现此问题的设计和/或编码决策。


1- 请参阅其他答案以获取使用注释、安全管理器等来隐藏堆栈跟踪内容以免影响应用程序员的示例。但请注意,在幕后,您正在每次方法调用时添加可能数百甚至数千条指令开销。

2- 不要低估程序员的良好意识。大多数程序员看到不调用某些方法的建议时,很可能会遵循该建议。


7
正确的做法是使用SecurityManager。定义一个权限,所有想要调用A.method()的代码都必须拥有该权限,然后确保只有B和A拥有该权限(这也意味着没有类拥有AllPermission)。在A中,您可以使用System.getSecurityManager().checkPermission(new BMethodPermission())进行检查,在B中,您可以在AccessController.doPrivileged(...)内部调用该方法。当然,这需要安装安全管理器(并使用适当的策略)-如果没有安装,所有代码都是可信的,每个人都可以调用任何东西(如果必要,可以使用反射)。

1
你可能也想在其中加入 doPrivileged。真是一团糟。 - Tom Hawtin - tackline
@Tom:我觉得那里有一个doPrivileged - Paŭlo Ebermann
1
原来是这样啊。仍然很混乱(需要修改public方法和奇怪的权限分配)。 - Tom Hawtin - tackline
@Tom:是的,你说得对,这不应该仅仅为了模块化而做,而只有在实际的安全概念需要时才应该这样做。 - Paŭlo Ebermann
有一个后续问题在 https://stackoverflow.com/q/50407042/14955,人们礼貌地要求提供代码示例。 - Thilo

6
您可以考虑使用接口。如果您正在传递调用类,您可以确认该类是否为适当的类型。
或者,如果您使用的是Java,则可以使用“默认”或“包”级别访问(例如void方法() vs. public void方法())。这将允许任何包内的类调用您的方法,并且不需要将类传递给方法。

我在考虑一个类似于接口的东西 - user489041

2

唯一确定在运行时检查的方法是获取堆栈跟踪。即使它是私有的,您也可以通过反射访问该方法。

一个更简单的方法是检查您的IDE中的用法。(前提是不通过反射调用)


1
+1 - 你也可以定义一个自定义注解来表达限制,并编写一个静态分析工具来检查限制是否被违反。(当然,反射可以规避这一点。) - Stephen C

2
正如其他人所提到的,使用堆栈跟踪是实现您所寻求功能的一种方法。通常情况下,如果需要“阻止”调用者访问公共方法,则可能表明设计不良。作为一个经验法则,应尽可能使用限制范围的访问修饰符。然而,将方法设置为包级私有或受保护的并不总是可行的。有时,您可能希望将某些类分组在单独的包中。在这种情况下,默认(包级私有)访问权限过于严格,而且通常没有子类化的意义,因此protected也没有帮助。
如果要限制对某些类的调用,则可以创建以下方法:
public static void checkPermission(Class... expectedCallerClasses) {
    StackTraceElement callerTrace = Thread.currentThread().getStackTrace()[3];
    for (Class expectedClass : expectedCallerClasses) {
        if (callerTrace.getClassName().equals(expectedClass.getName())) {
            return;
        }
    }
    throw new RuntimeException("Bad caller.");
}

使用它非常简单:只需指定哪个类可以调用该方法。例如,

public void stop() {
    checkPermission(ShutdownHandler.class);
    running = false;
}

因此,如果除了ShutdownHandler之外的类调用stop方法,checkPermission将抛出IllegalStateException异常。
你可能会想为什么checkPermission硬编码使用堆栈跟踪的第四个元素。这是因为Thread.getStackTrace()使最近调用的方法成为第一个元素。所以,
- getStackTrace()[0] 将是对getStackTrace本身的调用。 - getStackTrace()[1] 将是对checkPermission的调用。 - getStackTrace()[2] 将是对stop的调用。 - getStackTrace()[3] 将是调用stop的方法。这就是我们感兴趣的部分。
你提到想要从特定的类和方法中调用方法,但checkPermission只检查类名。添加检查方法名称的功能只需要进行一些修改,所以我将把它留作练习。

1

合理使用protected


如果我将该方法设置为protected,则无法使用它,因为B类不是A类的子类。而且A类是final的。 - Swaranga Sarma
@Swaranga Sarma - protected 关键字适用于同一包中的类。 - wkl
你可以设计成这样,否则就只能使用堆栈跟踪的低劣 hack。 - jmj
如果您可以稍微重构一下包结构,就可以利用“包私有”默认可见性。请参考我的答案。 - CodeFusionMobile
@birryree...我的B类已经存在,而且它在不同的包中。 - Bagmita

1

在Java中完成这个任务的标准方法是将B类和A类放在同一个包中(可能是当前应用程序的子包),并使用默认可见性。

默认的Java可见性是“包私有”,这意味着该包中的所有内容都可以看到您的方法,但该包之外的任何内容都无法访问它。

另请参阅:
是否有一种方法在Java中模拟C++的“友元”概念?


1
假设您只需要将此限制应用于项目内的类,则静态分析可能适合您 - 例如,ArchUnit测试:
package net.openid.conformance.archunit;

import com.google.gson.JsonElement;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.AccessTarget;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import net.openid.conformance.testmodule.OIDFJSON;
import org.junit.Test;

import static com.tngtech.archunit.core.domain.JavaCall.Predicates.target;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.assignableTo;
import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.*;
import static com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With.owner;
import static com.tngtech.archunit.lang.conditions.ArchPredicates.are;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

public class PreventGetAs {
    @Test
    public void doNotCallJsonElementGetAs() {
        JavaClasses importedClasses = new ClassFileImporter().importPackages("net.openid.conformance");

        JavaClasses allExceptOIDFJSON = importedClasses.that(DescribedPredicate.not(nameContaining("OIDFJSON")));

        ArchRule rule = noClasses().should().callMethodWhere(
            target(nameMatching("getAs[^J].*")) // ignores getAsJsonObject/getAsJsonPrimitive/etc which are fine
                .and(target(owner(assignableTo(JsonElement.class)))
        )).because("the getAs methods perform implicit conversions that might not be desirable - use OIDFJSON wrapper instead");

        rule.check(allExceptOIDFJSON);
    }
}

0

我知道你的使用情况中提到了“特定类中的特定方法”,但我认为你无法在设计时可靠地解决这个问题(而且我也想不出有哪种情况需要强制执行这个限制)。

以下示例创建了一个简单的设计时解决方案,用于限制访问类的方法仅限于特定类。但是,它可以轻松扩展到允许多个类。

通过定义一个公共内部类和一个私有构造函数来实现。该内部类作为所需方法的密钥。在下面的示例中,类Bar具有一个只能从Foo类的实例调用的方法。

类Foo:

public class Foo
{
    public Foo()
    {   
        Bar bar = new Bar();
        bar.method(new FooPrivateKey());
    }

    public class FooPrivateKey
    {
        private FooPrivateKey()
        {   }
    }  
}

类 Bar:

public class Bar
{
    public Bar()
    {

    }

    public void method(FooPrivateKey fooPrivateKey)
    {
        if(fooPrivateKey == null)
        {   throw new IllegalArgumentException("This method should only be called from the Foo class.");}

        //Do originally intended work.
    }
}

我认为这种方法并不适用于反射或者像 FooPrivateKey.class.newInstance() 这样的操作,但是它至少可以比简单的注释或文档更明显地提醒程序员,同时你也不必去研究像 Roberto TrunfioRonan Quillevere 所建议的更复杂的事情(这些也是完全可行的答案,只是在我看来对大多数情况来说过于复杂了)。

希望这对你的使用场景足够了。


0

您可以使用注释和反射来实现。我将报告一个类似的情况,即只能让外部类的特定方法调用该方法的情况。假设必须通过任何公共方法“保护”的类是Invoked,而Invoker是具有启用从Invoked调用一个或多个方法的方法的类。然后,您可以执行以下操作。

public class Invoked{

  @Retention(RetentionPolicy.RUNTIME)
  @Target(ElementType.METHOD)
  public static @interface CanInvoke{} 


   public void methodToBeInvoked() {
    boolean canExecute=false;
    try {
        //get the caller class
        StackTraceElement element = (new Throwable()).getStackTrace()[1];
        String className = element.getClassName();
        Class<?> callerClass = Class.forName(className);
        //check if caller method is annotated
        for (Method m : callerClass.getDeclaredMethods()) {
            if (m.getName().equals(methodName)) {
                if(Objects.nonNull(m.getAnnotation(EnabledToMakeOperationRemoved.class))){
                    canExecute = true;
                    break;
                }
            }
        }

    } catch (SecurityException | ClassNotFoundException ex e) {
        //In my case does nothing
    }
    if(canExecute){
      //do something
    }
    else{
      //throw exception
    }
   }
}

Invoker 类则是

public class Invoker{
   private Invoked i;

   @Invoked.CanInvoke
   public void methodInvoker(){
     i.methodToBeInvoked();
   }

}

请注意,启用调用的方法应带有CanInvoke注释。
您所请求的情况类似。您可以对不能调用公共方法的类/方法进行注释,然后仅在未注释的方法/类上设置canExecute变量为true

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接