How to prevent public methods from being called fr

2019-01-11 18:35发布

问题:

I have an existing class into which I want to add a method. But I want the method to be called only from a specific method from a specific class. Is there any way that I can prevent that call from other classes/methods?

For example, I have an existing class 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
    }
}

One way of doing this is getting the StackTrace inside the method() and then confirming the parent method?

What I am looking for is a solution that is more clean and advisable solution like a pattern or something.

回答1:

To be honest, you have painted yourself into a corner here.

If classes A and B are not related and not members of the same package, then visibility won't solve the problem. (And even if it did, reflection can be used to subvert the visibility rules.)

Static code analysis won't solve the problem if the code can use reflection to call the method.

Passing and checking B.this as an extra parameter to A.method(...) doesn't help because some other class C could pass a B instance.

This leaves only the stacktrace approach1... or giving up and relying on the good sense of the programmer2 not to call methods that they shouldn't.


The ideal solution is to revisit the design and/or coding decisions that got you into this mess.


1 - See other answers for examples that use annotations, a security manager, etc to conceal the stacktrace stuff from the application programmer. But note that under the hood you are adding probably hundreds, possibly thousands of instructions overhead per method call.

2 - Do not underestimate the programmer's good sense. Most programmers, when they see advice not to call some method, are likely to follow that advice.



回答2:

The right way to do this would be a SecurityManager.

Define a permission which all code which wants to call A.method() has to have, and then make sure only B and A have that permission (this also means that no class has AllPermission).

In A, you check this with System.getSecurityManager().checkPermission(new BMethodPermission()), and in B you call the method inside of AccessController.doPrivileged(...).

Of course, this requires that a security manager is installed (and it uses suitable policies) - if it isn't, all code is trusted and everyone can call everything (if necessary, with Reflection).



回答3:

You might consider using an interface. If you're passing in the calling class, you can confirm that the class is of the appropriate type.

Alternatively, if you're using Java, you can use "default" or "package" level access (e.g. void method() vs. public void method()). This will allow your method to be called by any class inside the package and does not require that you pass the class to the method.



回答4:

Make proper use of protected



回答5:

The only way to check for sure at run time is to take a stack trace. Even if its private you can access the method via reflections.

A simpler way to do this would be to check usages in your IDE. (provided its not called via reflections)



回答6:

As others have mentioned, using the stack trace is one way to implement the functionality that you are looking for. Generally, if one needs to "block" callers from a public method, it could be a sign of poor design. As a rule of thumb, use access modifiers that restrict the scope as much as possible. However, making a method package-private or protected is not always an option. Sometimes, one may want to group some classes in a separate package. In that case, the default (package-private) access is too restrictive, and it usually does not make sense to subclass, so protected is not helpful either.

If restricting calling to certain classes is desired, you can create a method like:

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.");
}

Using it is very simple: just specify what class(es) can call the method. For example,

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

So, if the stop method gets called by a class other than ShutdownHandler, checkPermission will throw an IllegalStateException.

You may wonder why checkPermission is hard-coded to use the fourth element of the stack trace. This is because Thread#getStackTrace() makes the most recently called method the first element. So,

  • getStackTrace()[0] would be the call to getStackTrace itself.
  • getStackTrace()[1] would be the call to checkPermission.
  • getStackTrace()[2] would be the call to stop.
  • getStackTrace()[3] would be the method that called stop. This is what we are interested in.

You mentioned that you want methods to be called from a specific class and method, but checkPermission only checks for class names. Adding the functionality to check for method names requires only a few modifications, so I'm going to leave that as an exercise.



回答7:

The standard way to do this in java is to put Class B and Class A in the same package (maybe a subpackage of your current application) and use the default visibility.

The default java visibility is "package-private" which means everything in that package can see your method, but nothing outside that package can access it.

See Also:
Is there a way to simulate the C++ 'friend' concept in Java?



回答8:

You can do it by using annotations and reflection. I will report a similar case, i.e. the case where you can let the method being called only by specific methods from extenal classes. Suppose that the class that must be "protected" by a whatsoever invocation of the its public methods is Invoked, while Invoker is the class tha has a method enabled to invoke one or more methods from Invoked. Then, you can do something like reported in the following.

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
    }
   }
}

and the Invoker class is

public class Invoker{
   private Invoked i;

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

}

Note that the method that is enabled to invoke is annotated with the CanInvoke annotation.

The case that you requested is similar. You annotate the classes/method that cannot call the public method and then you set to true the canExecute variable only if the method/class is not annotated.



回答9:

You can use a tool like Macker and add it to your build process to check some rules are respected, like

<?xml version="1.0"?>
<macker>    
    <ruleset name="Simple example">
        <access-rule>
            <deny>
                <from class="**Print*" />
                <to class="java.**" />
            </deny>
        </access-rule>
    </ruleset>
</macker>

It will NOT prevent you from writing wrong code but if you use Maven or another build system it can raise an error during your build process.

This tools work at a "class" level not at a "method" level but I do not see the point of preventing the call of only one method from a certain class ...



回答10:

I realise your use case states 'specific method in specific class', but I don't think you can reliably solve this at design time (and I can't think of a use case where this would have to be enforced anyway).

The following example creates an easy design time solution for restricting the access of a class' method to a particular class. It can, however, be easily extended to multiple allowed classes.

It is achieved by defining a public inner class with a private constructor that acts as a key to the method at hand. In the following example the class Bar has a method that should only be called from an instance of the Foo class.

Class Foo:

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

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

Class 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.
    }
}

I don't think this is by any means safe for things like reflection or even things like FooPrivateKey.class.newInstance(), but this will at least warn the programmer a little more obtrusively than a simple comment or documentation, while you don't have to look in to more complicated things like what was suggested by people like Roberto Trunfio and Ronan Quillevere (which are perfectly viable answers as well, just too complicated for most situations in my opinion).

I hope this is sufficient for your use case.