All About Proxies

So far, we have taken only a cursory look at the proxies generated by ProxyFactory. We mentioned that there are two types of proxy available in Spring: JDK proxies created using the JDK Proxy class and CGLIB-based proxies created using the CGLIB Enhancer class. Understanding the differences between these proxies is key to making the AOP code in your application perform as well as it can. In this section, we take a detailed look at the differences between the proxies and how these differences affect the performance of your application.

You may be wondering exactly what the difference between the two proxies is and why Spring needs two different types of proxy. Prior to version 1.1 of Spring, the two types of proxy shared much in common in the way they were implemented, and the performance of both types of proxy was very similar. The reason that there are two types of proxy is because of the poor performance of Proxy under JDK 1.3. Spring overcame this by providing CGLIB proxies, which performed comparably to the JDK 1.4 proxies when running on a 1.3 JVM.

The initial intention of the CGLIB proxy was to overcome the performance issues of the Proxy class on JDK 1.3, and the implementation was as similar to that of the JDK proxy as possible. The only drawback with this was that Spring was not taking full advantage of the feature-set available with CGLIB. In version 1.1, things have changed dramatically. The CGLIB proxy is now heavily optimized and outperforms the JDK proxy quite dramatically in many cases. Before we take a look at the differences in proxy implementations, we first must make sure you understand exactly what the generated proxies have to do.

Caution 

Before version 1.1 of Spring, a bug in the CGLIB proxy code resulted in an inordinate amount of dynamically generated classes being created unnecessarily. In the long run, this resulted in excess memory being used and eventually OutOfMemoryErrors occurring. This bug is fixed in Spring 1.1, so if you are having problems with earlier versions of Spring related to memory usage, you might want to upgrade.

Understanding Proxies

The core goal of a proxy is to intercept method invocations, and where necessary, execute chains of advice that apply to a particular method. The management and invocation of advice is largely proxy independent and is managed by the Spring AOP framework. However, the proxy is responsible for intercepting calls to all methods and passing them as necessary to the AOP framework for the advice to be applied.

In addition to this core functionality, the proxy must also support a set of additional features. It is possible to configure the proxy to expose itself via the AopContext class so that you can retrieve the proxy and invoke advised methods on the proxy from the target object. The proxy is responsible for ensuring that when this option is enabled via ProxyFactory.setExposeProxy(), the proxy class is appropriately exposed. In addition to this, all proxy classes implement the Advised interface by default, which allows for, among other things, the advice chain to be changed after the proxy has been created. A proxy must also ensure that any methods that return this—that is, return the proxied target—do in fact return the proxy and not the target.

As you can see, a typical proxy has quite a lot of work to perform, and all of this logic is implemented in both the JDK and CGLIB proxies. As of version 1.1 of Spring, the way in which this logic is implemented differs quite drastically depending on which of the proxy types you are using.

Using JDK Dynamic Proxies

JDK proxies are the most basic type of proxy available in Spring. Unlike the CGLIB proxy, the JDK proxy can only generate proxies of interfaces, not classes. In this way, any object you wish to proxy must implement at least one interface. In general, it is good design to use interfaces for your classes, but it is not always possible, especially when you are working with third-party or legacy code. In this case, you must use the CGLIB proxy.

When you are using the JDK proxy, all method calls are intercepted by the JVM and routed to the invoke() method of the proxy. This method then determines whether or not the method in question is advised, and if so, it invokes the advice chain and then the method itself using reflection. In addition to this, the invoke() method performs all the logic discussed in the previous section.

The JDK proxy makes no determination between methods that are advised and unadvised until it is in the invoke() method. This means that for unadvised methods on the proxy, the invoke() method is still called, all the checks are still performed, and the method is still invoked using reflection. Obviously this incurs a runtime overhead each time the method is invoked, even though the proxy often performs no additional processing other than to invoke the unadvised method via reflection.

You can instruct the ProxyFactory to use a JDK proxy by specifying the list of interfaces to proxy using setProxyInterfaces().

Using CGLIB Proxies

With the JDK proxy, all decisions about how to handle a particular method invocation are handled at runtime each time the method is invoked. When you use CGLIB, you avoid this approach in favor of one that performs much better. A full discussion of the inner workings of CGLIB is well beyond the scope of this chapter, but essentially CGLIB dynamically generates the bytecode for a new class on the fly for each proxy, reusing already generated classes wherever possible. This approach allows you to make extensive optimizations.

When a CGLIB proxy is first created, CGLIB asks Spring how it wants to handle each method. This means that many of the decisions that are performed in each call to invoke() on the JDK proxy are performed just once for the CGLIB proxy. Because CGLIB generates actual bytecode, there is also a lot more flexibility in the way you can handle methods. For instance, the CGLIB proxy generates the appropriate bytecode to invoke any unadvised methods directly, dramatically reducing the overhead introduced by the proxy. In addition to this, the CGLIB proxy determines whether it is possible for a method to return this; if not, it allows the method call to be invoked directly, again reducing the overhead substantially.

The CGLIB proxy also handles fixed advice chains differently than the JDK proxy. A fixed advice chain is one that you guarantee will not change after the proxy has been generated. By default, you are able to change the advisors and advice on a proxy even after it is created, although this is rarely a requirement. The CGLIB proxy handles fixed advice chains in a particular way, reducing the runtime overhead for executing an advice chain.

In addition to all these optimizations, the CGLIB proxy utilizes the bytecode generation capabilities to gain a slight increase in performance when invoking advised methods; this results in advised methods that perform slightly better than those on JDK proxies.

Comparing Proxy Performance

So far, all we have done is discuss in loose terms the differences in implementation between the different proxy types. In this section, we are going to run a simple performance test to compare the performance of the CGLIB proxy with the JDK proxy.

Listing 6-34 shows the code for the performance test.

Listing 6-34: Testing Proxy Performance

image from book
package com.apress.prospring.ch6.proxies;      import org.springframework.aop.Advisor; import org.springframework.aop.framework.Advised; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.support.DefaultPointcutAdvisor;      public class ProxyPerfTest {          public static void main(String[] args) {         ISimpleBean target = new SimpleBean();              Advisor advisor = new DefaultPointcutAdvisor(new TestPointcut(),                 new NoOpBeforeAdvice());              runCglibTests(advisor, target);         runCglibFrozenTests(advisor, target);         runJdkTests(advisor, target);     }          private static void runCglibTests(Advisor advisor, ISimpleBean target) {         ProxyFactory pf = new ProxyFactory();         pf.setTarget(target);         pf.addAdvisor(advisor);                       ISimpleBean proxy = (ISimpleBean)pf.getProxy();         System.out.println("Running CGLIB (Standard) Tests");         test(proxy);     }          private static void runCglibFrozenTests(Advisor advisor, ISimpleBean target) {         ProxyFactory pf = new ProxyFactory();         pf.setTarget(target);         pf.addAdvisor(advisor);         pf.setFrozen(true);                  ISimpleBean proxy = (ISimpleBean)pf.getProxy();         System.out.println("Running CGLIB (Frozen) Tests");         test(proxy);     }          private static void runJdkTests(Advisor advisor, ISimpleBean target) {         ProxyFactory pf = new ProxyFactory();         pf.setTarget(target);         pf.addAdvisor(advisor);         pf.setInterfaces(new Class[]{ISimpleBean.class});                  ISimpleBean proxy = (ISimpleBean)pf.getProxy();         System.out.println("Running JDK Tests");         test(proxy);     }          private static void test(ISimpleBean bean) {         long before = 0;         long after = 0;                  // test advised method         System.out.println("Testing Advised Method");         before = System.currentTimeMillis();         for(int x = 0; x < 500000; x++) {             bean.advised();         }         after = System.currentTimeMillis();;                  System.out.println("Took " + (after - before) + " ms");                  // testing unadvised method         System.out.println("Testing Unadvised Method");         before = System.currentTimeMillis();          for(int x = 0; x < 500000; x++) {             bean.unadvised();         }         after = System.currentTimeMillis();;                  System.out.println("Took " + (after - before) + " ms");                  // testing equals() method         System.out.println("Testing equals() Method");         before = System.currentTimeMillis();          for(int x = 0; x < 500000; x++) {             bean.equals(bean);         }         after = System.currentTimeMillis();;                  System.out.println("Took " + (after - before) + " ms");                  // testing hashCode() method         System.out.println("Testing hashCode() Method");         before = System.currentTimeMillis();          for(int x = 0; x < 500000; x++) {             bean.hashCode();         }         after = System.currentTimeMillis();;                  System.out.println("Took " + (after - before) + " ms");                  // testing method on Advised         Advised advised = (Advised)bean;                  System.out.println("Testing Advised.getProxyTargetClass() Method");         before = System.currentTimeMillis();          for(int x = 0; x < 500000; x++) {             advised.getProxyTargetClass();         }         after = System.currentTimeMillis();;                  System.out.println("Took " + (after - before) + " ms");                  System.out.println(">>>\n");     } }
image from book

In this code you can see that we are testing three kinds of proxy: a standard CGLIB proxy, a CGLIB proxy with a frozen advice chain, and a JDK proxy. For each proxy type, we run the following five test cases:

  • Advised method: A method that is advised. The advice type used in the test is a before advice that performs no processing so it reduces the effects of the advice itself on the performance tests.

  • Unadvised method: A method on the proxy that is unadvised. Often your proxy has many methods that are not advised. This test looks at how well unadvised methods perform for the different proxies.

  • The equals() method: This test looks at the overhead of invoking the equals() method. This is especially important when you use proxies as keys in a HashMap or similar collection.

  • The hashCode() method: As with the equals() method, the hashCode() method is important when you are using HashMaps or similar collections.

  • Executing methods on the Advised interface: As we mentioned earlier, a proxy implements the Advised interface by default, allowing you to modify the proxy after creation and to query information about the proxy. This test looks at how quick methods on the Advised interface can be accessed using the different proxy types.

We ran the test on a Pentium 4 3.0GHz machine with 2GB of RAM. When running the test, we set the initial heap size of the JVM to 1024MB to reduce the effects of heap resizing on test results. The results are shown in Table 6-3.

Table 6-3: Proxy Performance Test Results (ms)
 

CGLIB (Standard)

CGLIB (Frozen)

JDK

Advised Method

343

172

516

Unadvised Method

47

47

391

equals()

47

31

343

hashCode()

16

16

391

Advised.getProxyTargetClass()

0

16

266

From the results in this table, it is clear to see that the CGLIB proxy performs much better than the JDK proxies. A standard CGLIB proxy only performs marginally better than the JDK proxy when executing an advised method, but there is a noticeable difference when you are using a proxy with a frozen advice chain. For unadvised methods, the CGLIB proxy is over eight times faster than the JDK proxy. Similar figures apply to the equals() and hashCode() methods, which are noticeably faster when you are using the CGLIB proxy. Notice that hashCode() is faster than equals(). The reason for this is that equals() is handled in a specific way to ensure that the equals() contract is preserved for the proxies. For methods on the Advised interface, you will notice that they are also faster on the CGLIB proxy, although not to the same degree. The reason for this is that Advised methods are handled early on in the intercept() method and they avoid much of the logic that is required for other methods.

From the test results, notice that for the standard CGLIB proxy, the invocation on Advised.getProxyTargetClass() took 0 milliseconds. This indicates that this call was optimized out by the JIT compiler. On subsequent runs of the test, we noticed that sometimes calls to hashCode() were also optimized out when either CGLIB proxy was used. Interestingly, none of the method calls was ever optimized out for the JDK proxy.

Which Proxy to Use?

Deciding which proxy to use is typically an easy decision. The CGLIB proxy can proxy both classes and interfaces, whereas the JDK proxy can only proxy interfaces. Add to this the fact that the CGLIB proxy clearly performs better than the JDK proxy and it becomes apparent that the CGLIB proxy is the correct choice. The only thing to be aware of when using the CGLIB proxy is that a new class is generated for each distinct proxy, although with version 1.1, the reuse of proxy classes is now functioning correctly, reducing the runtime overhead of frequent class generation and reducing the amount of memory used by the CGLIB proxy classes. When proxying a class, the CGLIB proxy is the default choice because it is the only proxy capable of generating a proxy of a class. In order to use the CGLIB proxy when proxying an interface, you must set the value of the optimize flag in the ProxyFactory to true using the setOptimize() method.



Pro Spring
Pro Spring
ISBN: 1590594614
EAN: 2147483647
Year: 2006
Pages: 189

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net