• Mitchell Bösecke

Java Proxies: From Dog to TransactionalDog

Updated: Dec 28, 2020

I didn't want to learn Java proxies but all of my software development tools were using proxies and it became impossible to use those tools without a thorough understanding of the underlying tech. These third-party libraries were truly leaving me in a dark haze of confusion and despair. Hold my hand and let's build a Frankenstein dog together.



What is a proxy?


Proxy is a design pattern where you substitute a real object with a look-alike object that mimics the real object. This fake look-alike accepts all your requests, does some extra work, and then passes the request to the real object. If it does its job well, you don't even know you're interacting with a proxy.


Java implemented a class called java.lang.reflect.Proxy that does exactly this.


Why would I make a proxy? I've been programming professionally for 10 years and I've never actually made a proxy. So I guess the answer is never. Third-party libraries use them to make your life "easier" but it ends up causing a lot of confusion so just make sure you learn them (leaky abstraction much?). We'll discuss some concrete use-cases at the end.


Example


In school, all examples are an Animal interface with Dog/Cat implementations and I absolutely hate that because it doesn't reflect anything meaningful in our jobs. Then again, neither do proxies, so let's double-down:



interface Animal {
    void makeNoise();
}

class Dog implements Animal {
    public void makeNoise(){
        System.out.print("woof!");
    }
}

Let's test this trivial nonsense:


Animal dog = new Dog();
dog.makeNoise(); // prints "woof!" like a good boy

Now let's create a proxy object around our Dog for no apparent reason. This proxy will intercept every method invocation and print "INTERCEPTED" before the actual method invocation. First, Java requires us to make this weird "InvocationHandler" class to handle this logic:



class AnimalHandler implements InvocationHandler {
    
    private final Animal originalAnimal;

    public AnimalHandler(Animal originalAnimal) {
        this.originalAnimal = originalAnimal;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
        System.out.print("INTERCEPTED ");
        method.invoke(originalAnimal, args); // invoke original method
        return null;
    }
}


Then we can build our actual Frankenstein proxy dog:



// Build a proxy with our AnimalHandler logic
Animal proxyDog = (Animal)       
    Proxy.newProxyInstance(Animal.class.getClassLoader(), new Class[]{Animal.class}, new AnimalHandler(dog));

proxyDog.makeNoise(); // "INTERCEPTED woof!" like a weird boy


We've done it. We've "wrapped" our actual dog implementation into a proxy. Every time we invoke a method on the proxy dog, our AnimalHandler intercepts it where it will perform it's own logic before it delegates the request to the underlying actual dog.


Let's take a closer look at that "proxyDog" variable. What type of variable is that? Its type is originally initialized as an Animal but is it also truly an instance of Dog?


System.out.println(dog.getClass()); // com.mitchellbosecke.Dog
System.out.println(proxyDog.getClass()); // com.sun.proxy.$Proxy0

System.out.println(dog instanceof Animal); // true
System.out.println(proxyDog instanceof Animal); // true

System.out.println(dog instanceof Dog); // true
System.out.println(proxyDog instanceof Dog); // false! hol up

Let me explain what happened here. At runtime, it constructed a brand new, never before existing class called "$Proxy0" which is a child of Animal and mimics Dog but isn't actually a dog. Even though the proxy isn't a real dog, it has an internal dog variable that it delegates all requests to (after performing its own custom logic). Here are a couple of helpful diagrams:



An interesting take-away is that you must have a common interface that the proxy can share with the original object; in this case, it's the Animal interface. Without that interface, none of this would work.


Example Use Cases


Spring Data JPA: anytime you use the @Transactional annotation on top of a component that is @Autowired somewhere else; spring will secretly inject a proxy of the original object. This proxy intercepts requests and starts a new database transaction.


Spring Security: anytime you use the @PreAuthorize annotation on your service-tier which is @Autowired elsewhere; spring will secretly inject a proxy of the original object. This proxy intercepts requests and checks security before invoking the actual method on the original object.


Hibernate: When you retrieve an object from the database, Hibernate secretly gives you a proxy of the original object. This is used to implement lazy-loading where it will fetch more data from the database if you invoke some of the getter methods on the proxy.


Conclusion


You will probably never build proxies yourself but it's important to understand that a lot of these common libraries secretly use them and trick you into thinking you're working with the original objects. This is how they perform their magic.



3,113 views0 comments
 

©2020 by Mitchell Bösecke.

This site was designed with the
.com
website builder. Create your website today.
Start Now