Hi! Today we will consider a rather important and interesting topic: the creation of dynamic proxy classes in Java. It's not very simple, so we'll try to figure it out using examples :) Dynamic proxies - 1So, the most important question: what are dynamic proxies and what are they for? A proxy class is a kind of "add-on" on top of the original class, which allows us to change the original class's behavior if necessary. What does it mean to "change behavior and how does that work? Consider a simple example. Suppose we have a Person interface and a simple Man class that implements this interface
public interface Person {

   public void introduce(String name);

   public void sayAge(int age);

   public void sayWhereFrom(String city, String country);
}

public class Man implements Person {

   private String name;
   private int age;
   private String city;
   private String country;

   public Man(String name, int age, String city, String country) {
       this.name = name;
       this.age = age;
       this.city = city;
       this.country = country;
   }

   @Override
   public void introduce(String name) {

       System.out.println("My name is " + this.name);
   }

   @Override
   public void sayAge(int age) {
       System.out.println("I am " + this.age + " years old");
   }

   @Override
   public void sayWhereFrom(String city, String country) {

       System.out.println("I'm from " + this.city + ", " + this.country);
   }

   // ...getters, setters, etc.
}
Our Man class has 3 methods: introduce, sayAge, and sayWhereFrom. Imagine that we got this class as part of an off-the-shelf JAR library and we can't simply rewrite its code. But we also need to change it behavior. For example, we don't know which method might be called on our object, but we want our person to say "Hi!" (no one likes someone who is impolite) when any of the methods are called. Dynamic proxies - 2What should we do in this situation? We'll need a few things:
  1. InvocationHandler

What is this? InvocationHandler is a special interface that lets us intercept any method call to our object and add the additional behavior we need. We need to create our own interceptor, i.e. create a class that implements this interface. This is pretty simple:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class PersonInvocationHandler implements InvocationHandler {

private Person person;

public PersonInvocationHandler(Person person) {
   this.person = person;
}

 @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

       System.out.println("Hi!");
       return null;
   }
}
We need to implement only one interface method: invoke(). And, by the way, it does what we need: it intercepts all method calls to our object and adds the necessary behavior (inside the invoke() method, we output "Hi!" to the console).
  1. The original object and its proxies.
We create our original Man object and an "add-on" (proxy) for it:
import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

       // Create the original object
       Man arnold = new Man("Arnold", 30, "Thal", "Austria");

       // Get the class loader from the original object
       ClassLoader arnoldClassLoader = arnold.getClass().getClassLoader();

       // Get all the interfaces that the original object implements
       Class[] interfaces = arnold.getClass().getInterfaces();

       // Create a proxy for our arnold object
       Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));

       // Call one of our original object's methods on the proxy object
       proxyArnold.introduce(arnold.getName());

   }
}
This doesn't look very simple! I specifically added a comment for each line of code. Let's take a closer look at what's going on. In the first line, we simply make the original object for which we will create proxies. The following two lines may cause you difficulty:
// Get the class loader from the original object
ClassLoader arnoldClassLoader = arnold.getClass().getClassLoader();

// Get all the interfaces that the original object implements
Class[] interfaces = arnold.getClass().getInterfaces();
Actually, there's nothing really special happening here :) In the fourth line, we use the special Proxy class and its static newProxyInstance() method:
// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
This method just creates our proxy object. We pass to the method the information about the original class, which we received in the last step (its ClassLoader and a list of its interfaces), as well as the previously created InvocationHandler object. The main thing is to not forget to pass our original arnold object to the invocation handler, otherwise there will be nothing to "handle" :) What did we end up with? We now have a proxy object: arnoldProxy. It can call any methods of the Person interface. Why? Because we gave it a list of all the interfaces here:
// Get all the interfaces that the original object implements
Class[] interfaces = arnold.getClass().getInterfaces();

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
Now it knows about all the methods of the Person interface. In addition, we passed to our proxy a PersonInvocationHandler object configured to work with the arnold object:
// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
Now if we call any method of the Person interface on the proxy object, our handler intercepts the call and executes its own invoke() method instead. Let's try to run the main() method! Console output:
Hi!
Excellent! We see that instead of the original Person.introduce() method, the invoke() method of our PersonInvocationHandler() is called:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

   System.out.println("Hi!");
   return null;
}
"Hi!" is displayed on the console, but this is not exactly the behavior that we wanted :/ What we were trying to achieve is to first display "Hi!” and then call the original method itself. In other words, the method call
proxyArnold.introduce(arnold.getName());
should display "Hi! My name is Arnold", not simply "Hi!" How can we achieve this? It's not complicated: we just need to take some liberties with our handler and the invoke() method :) Pay attention to what arguments are passed to this method:
public Object invoke(Object proxy, Method method, Object[] args)
The invoke() method has access to the originally invoked method, and to all its arguments (Method method, Object[] args). In other words, if we call the proxyArnold.introduce(arnold.getName())method so that the invoke() method is called instead of the introduce() method, then inside this method we have access to the original introduce() method and its argument! As a result, we can do this:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class PersonInvocationHandler implements InvocationHandler {

   private Person person;

   public PersonInvocationHandler(Person person) {

       this.person = person;
   }

   @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       System.out.println("Hi!");
       return method.invoke(person, args);
   }
}
Now in the invoke() method we have added a call to the original method. If we now try to run the code from our previous example:
import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

       // Create the original object
       Man arnold = new Man("Arnold", 30, "Thal", "Austria");

       // Get the class loader from the original object
       ClassLoader arnoldClassLoader = arnold.getClass().getClassLoader();

       // Get all the interfaces that the original object implements
       Class[] interfaces = arnold.getClass().getInterfaces();

       // Create a proxy for our arnold object
       Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));

       // Call one of our original object's methods on the proxy object
       proxyArnold.introduce(arnold.getName());
   }
}
then we'll see that now everything works as it should :) Console output:
Hi! My name is Arnold
When might you need this? Actually, quite often. The "dynamic proxy" design pattern is actively used in popular technologies... Oh, by the way, I forgot to mention that Dynamic Proxy is a design pattern! Congratulations, you learned one more! :) Dynamic proxies - 3For example, it is actively used in popular technologies and frameworks related to security. Imagine that you have 20 methods that should only be executed by users who are signed in to your program. Using the techniques you have learned, you could easily add to these 20 methods a check to see whether the user has entered valid credentials without duplicating the verification code in each method. Or suppose you want to create a log where all user actions will be recorded. This is also easy to do using a proxy. Even now, you could simply add code to our example above so that the method name is displayed when you call invoke(), and that would produce a super simple log of our program :) In conclusion, pay attention to one important limitation. A proxy object works with interfaces, not classes. A proxy is created for an interface. Take a look at this code:
// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
Here we create a proxy specifically for the Person interface. If we try to create a proxy for the class, i.e. change the type of reference and try to cast to the Man class, it won't work.
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));

proxyArnold.introduce(arnold.getName());
Exception in thread "main" java.lang.ClassCastException: com.sun.proxy.$Proxy0 cannot be cast to Man Having an interface is an absolute requirement. Proxies work with interfaces. That's all for today :) Well, now it would be good to solve a few tasks! :) Until next time!