Chapter 6. Services and AOP

Most kayakers that I know are demented and proud of it. We tend to look for those places on a river that are dangerous enough to scare but tame enough to play. While I was learning to surf hydraulics on Barton Creek at high water, I slipped into a hydraulic to play, but couldn’t get out. I flipped, upstream, and the hydraulic quickly flipped me 360 degrees, so that I was upright again. I frantically surfed a little while longer, then flipped again, and then all of my control deserted me. The hydraulic then took over, and spun me around like a window shade. When my friends laughed and grabbed their cameras instead of their safety ropes, I bailed, coughing and gagging my way to the bank to nurse my battered ego. I mumbled something about my boat being the source of the problem. Predictably, one of my friends got into my boat, and then toyed with my nemesis, the killer hydraulic, for a while. To make things worse, he then threw his paddle onto the bank, and played some more. He said, “Bruce, it’s not the boat. It’s how you use it.” I’m still fuming today.

Building a Service

The core functions of your application are roughly complete, but it’s going to be hard to trace what changes a user made, and when. In this example, you’ll build an audit trail that records a log record whenever someone takes an action that changes the database. Rather than add the same code in many different places, you’ll build a simple Spring service to do the job. You’ll focus on the Hibernate implementation that you built earlier.

Object-oriented languages handle some problems pretty well, but they don’t handle crosscutting concerns, common in enterprise developments, very well. Figure 6-1 shows a method with some common enterprise services that crosscut many methods in DAO and façade layers.

Some classic enterprise crosscutting concerns
Figure 6-1. Some classic enterprise crosscutting concerns

To handle crosscutting concerns, you might decide to use a container. Most containers accept only components that match their specification, and provide services to components once they’re in the container. Heavyweight container architectures, like EJB 1.x and 2.x, handle crosscutting concerns, but they force you to write code that depends on your given container. You can’t take the code out of the container and expect it to run, and since the container takes too long to start, the result is difficult to test.

Lightweight containers that use AOP technologies accept components too, but the component is a simple POJO. You can then use AOP to attach services to the POJO. We’ll get into how this works later in the chapter, but for now, think of this feature as the ability to add declarative services to POJO. In Spring, you implement declarative services with configuration rather than code, so you can change services on the fly through changes in the context.

How do I do that?

The interceptor strategy uses three objects: the target (our façade), a proxy object that Spring creates for you, and an interceptor, which you’ll build. Essentially, the proxy imitates the target—wherever you would use an instance of the target object, Spring will use the proxy instead. The proxy will pass all calls to the object through a series of interceptors that you’ll configure in the context. Depending on the type of interceptors, the call might pass through the interceptor before reaching the target, on the way back from the target, or both.

You’ve got to do three things:

  1. Create your service, which we’ll call an interceptor. Another word for interceptors is advice.

  2. Configure the beans: the interceptor and the target object.

  3. Configure the proxy—identify the proxy, the target object, and the methods that will use our service.

To build advice, you’ll simply implement an interface. Think of advice as something that will happen at some event in a program’s execution. Spring has four types of advice:

Before advice

Spring fires this type of advice before it invokes a method that you specify, called the target method. This type of advice implements the MethodBeforeAdvice interface.

After Returning advice

Spring fires this type of advice after returning from your target method. After Returning advice implements the AfterReturningAdvice interface.

Throws advice

Spring fires this type of advice when your target method throws an exception. This type of advice implements the ThrowsAdvice interface.

Around advice

Spring fires this type of advice before a method, and allows you to choose whether to invoke the target method. After the target method returns, you can add additional code. This is also called an interceptor.

In general, you should use the simplest type of advice that will solve your problem. For this example, you’ll use before advice, and you’ll build on the Hibernate interface. The interceptor will implement the MethodBeforeAdvice interface. It will do the logging. Don’t worry about how it gets invoked just yet. Example 6-1 gives the advice.

Example 6-1. LoggingBefore.java
public class LoggingBefore implements MethodBeforeAdvice {

    private SessionFactory factory;

    public SessionFactory getFactory( ) {
        return factory;
    }

    public void setFactory(SessionFactory factory) {
        this.factory = factory;
    }

    public void before(Method method, Object[] objects, Object o) 
           throws Throwable {

        Session s = null;
        LogEvent le = new LogEvent(method.getName( ), new Date( ));
        try {
            s = factory.openSession( );
            s.save(le);
        } catch (Exception ex) {
            //log the exception
        } finally {
            s.close( );
        }
    }
}

For now, you can manually invoke it in a test case. In this case, you can simply count the number of events that exist in the database prior to running the advice, then compare that to the number of events in the database after running it. For the test in Example 6-2, it doesn’t matter which method is being logged, just that the logger is activated.

Example 6-2. ControllerTest.java
public void testLocal( ) throws Exception {
    LoggingBefore lb = new LoggingBefore( );
    SessionFactory factory = 
           (SessionFactory)ctx.getBean("sessionFactory");

    // getCurEventsCount( ) is a private utility method that
    // gets the current number of log events in the database
    int initialCount = getCurEventsCount( );

    lb.setFactory(factory);
    try {
          lb.before((Method)this.getClass( ).getMethods( )[0], null, null);
    } catch (Throwable t) {
           fail(t.getMessage( ));
    }

    int afterCount = getCurEventsCount( );
    assertEquals("Should add one event to log", 
           initialCount, afterCount - 1);
}

What just happened?

You’ve just built a service. Unlike other container architectures, Spring does not define the complete package of services that components in the container can use. The services are ideal for our purposes:

  • The clean programming model defines exactly how a service must be packaged.

  • The services strategy is minimal, requiring very little of the service builder.

  • The services are easy to test, since they can run outside of the container.

  • The services can be used elsewhere; they are compatible with a standard called the Aspect Alliance.

Of course, right now, the service doesn’t do anything. You need to attach it to something real.

Configuring a Service

You’ve built an audit service that will help you keep track of changes in the application. Now, you need to attach the service to your code. You’ll configure the service in this lab.

How do I do that?

The interceptor strategy uses three objects: the target (our façade), a proxy object that Spring creates for you, and an interceptor, which you’ll build. Recall that you’ve got to do three things:

  1. Configure the advice.

  2. Configure the advisor, including a target object, target methods, and the advice.

  3. Configure the target object.

  4. Configure the proxy to use the advisor.

The target object already exists: our façade. The proxy already exists, because you used it for transactions. You need to configure the advice and add it to your proxy. Example 6-3 shows the changes to the context.

Example 6-3. RentABike-servlet.xml
<beans>   
   <bean id="transactionManager" 
   class="org.springframework.orm.hibernate.HibernateTransactionManager">
      <property name="sessionFactory">
         <ref local="sessionFactory"/>
      </property>
   </bean>

   <bean id="transactionInterceptor" 
class="org.springframework.transaction.interceptor.TransactionInterceptor">
      <property name="transactionManager">
         <ref local="transactionManager"/>
      </property>
      <property name="transactionAttributeSource">
         <value>
            com.springbook.HibernateRentABike.save*=PROPAGATION_REQUIRED
         </value>
      </property>
   </bean>

   <bean id="loggingBeforeInterceptor" 
      class="com.springbook.interceptors.LoggingBefore">
      <property name="factory">
         <ref local="sessionFactory"/>
      </property>
   </bean>

   <bean id="rentaBikeTarget" class="com.springbook.HibRentABike">
      <property name="storeName">
         <value>Bruce's Bikes</value>
      </property>
      <property name="sessionFactory">
         <ref local="sessionFactory"/>
      </property>
   </bean>

   <bean id="rentaBike" 
      class="org.springframework.aop.framework.ProxyFactoryBean">
      <property name="proxyInterfaces">
         <value>com.springbook.RentABike</value>
      </property>
      <property name="interceptorNames">
         <list>
            <value>loggingBeforeInterceptor</value>
            <value>transactionInterceptor</value>
            <value>rentaBikeTarget</value>
         </list>
      </property>
   </bean>

   <!-- etc. -->
</beans>

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
   <property name="driverClassName">
      <value>com.mysql.jdbc.Driver</value>
   </property>
   <property name="url">
      <value>jdbc:mysql://localhost/bikestore</value>
   </property>
   <property name="username"><value>bikestore</value></property>
</bean>

Now, you can let it rip. Notice that you get an audit trail whenever you change any record.

What just happened?

So far, most Java developers use programmatic services—you load them and access them explicitly. Spring also allows declarative services—you specify them in configuration rather than code—with proxy objects. A proxy object stands in for a target object, forwarding calls to the target after providing its service. The proxy and its target have the same interface. CORBA used proxies to do distributed communication, without forcing us to create any code for distribution: the intelligence was in the proxy. With AOP, the job of the proxy is to apply advice to remote objects.

Our example uses simple before advice. Now, let’s look at around advice, the most complex model. If you understand the most advanced flow, the others should be easy. Figure 6-2 shows how advice works in Spring. The BikeDAO is the target object. It has the business code. To configure an interceptor, you specify a proxy. The proxy maintains a chain of interceptors. An object calls a method on the proxy instead of the target object. When you call a method on the proxy or fire an exception, the proxy calls the first interceptor in the chain. Each interceptor does its work and then invokes the next interceptor in the chain. The last interceptor invokes the target method or exception. After the target does its work, the interceptors just return right back down the chain.

Spring attaches services to POJOs through proxies that call an interceptor chain
Figure 6-2. Spring attaches services to POJOs through proxies that call an interceptor chain

In this example, you configured a proxy on a target object. You told the proxy to apply your interceptor to any method in the target, but you could have just as easily specified a subset of the methods with a regular expression. Example 6-4 is the same advice, coded as an interceptor.

Example 6-4. LoggingAround.java
public class LoggingAround implements MethodInterceptor {
    private SessionFactory factory;

    public SessionFactory getFactory( ) {
        return factory;
    }

    public void setFactory(SessionFactory factory) {
        this.factory = factory;
    }

    private void logEvent(String methodName, String message) 
           throws Exception {

        Session s = null;
        LogEvent le = new LogEvent(methodName, new Date( ), message);
        try {
            s = factory.openSession( );
            s.save(le);
        } catch (Exception ex) {
            //log the exception
        } finally {
            s.close( );
        }
    }

    public Object invoke(MethodInvocation methodInvocation)
           throws Throwable {
        logEvent(methodInvocation.getMethod( ).getName( ), "Entering call.");
        Object result = methodInvocation.proceed( );
        logEvent(methodInvocation.getMethod( ).getName( ), "Leaving call.");
        return result;
    }
}

Example 6-5 shows the new configuration.

Example 6-5. RentABike-servlet.xml
<bean id="loggingAround" class="com.springbook.interceptors.LoggingAround">
   <property name="factory"><ref local="sessionFactory"/></property>
</bean>

<bean id="saveAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
   <property name="advice">
      <ref local="loggingAround"/>
   </property>
   <property name="patterns">
      <list>
         <value>.*save.*</value>
      </list>
   </property>
</bean>

<bean id="rentaBikeTarget" class="com.springbook.HibRentABike">
    <property name="storeName"><value>Bruce's Bikes</value></property>
    <property name="sessionFactory"><ref local="sessionFactory"/></property>
</bean>

<bean id="rentaBike" 
    class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="proxyInterfaces">
       <value>com.springbook.RentABike</value>
    </property>
    <property name="interceptorNames">
       <list>
          <value>transactionInterceptor</value>
          <value>saveAdvisor</value>
          <value>rentaBikeTarget</value>
       </list>
    </property>
</bean>

The table results in Example 6-6 show the before and after advice being called only for save-related methods.

Example 6-6. Examining the contents of the logging table
+---------+--------------+------------+----------------+
| eventId | methodName   | dateTime   | message        |
+---------+--------------+------------+----------------+
|      14 | saveBike     | 2004-10-13 | Entering call. |
|      15 | saveBike     | 2004-10-13 | Leaving call.  |
|      16 | saveCustomer | 2004-10-13 | Entering call. |
|      17 | saveCustomer | 2004-10-13 | Leaving call.  |
|      18 | saveBike     | 2004-10-13 | Entering call. |
|      19 | saveBike     | 2004-10-13 | Leaving call.  |
+---------+--------------+------------+----------------+

The end result is exactly what we expect. Each time one of the specified methods gets called on our target, the advice fires. We’ve got declarative auditing. In fact, we can code any service and make it declarative.

Using an Autoproxy

So far, you’ve explicitly and manually created each proxy. Spring also has the ability to automatically create a proxy, or autoproxy. The goal of the autoproxy is to apply the same signature to a number of methods. In this lab, you’ll use an autoproxy to attach profiling code to all of the classes in the application.

How do I do that?

Spring 1.1 lets you do autoproxying in two ways. You can:

  • Specify a configuration and apply it to named beans context.

  • Use source-level metadata (added to the JDK in Java 5, but available through Apache Commons annotations today).

In this example, you’re going to be proxying named beans. As before, you’ll first need some advice. Example 6-7 shows around advice that prints profiling statements before and after Spring enters a method.

Example 6-7. ProfilingInterceptory.java
public class ProfilingInterceptor implements MethodInterceptor{
    public Object invoke(MethodInvocation methodInvocation) 
           throws Throwable {
        long start = System.currentTimeMillis( );
        Object results = methodInvocation.proceed( );
        long end = System.currentTimeMillis( );
        System.out.println("Method: " + 
           methodInvocation.getMethod( ).getName( ) + " took " +
           (end - start) + " ms.");
        return results;
    }
}

Next, configure it. Don’t specify a single target, but a regular expression. Spring will apply the advice to all of the beans that match. Specify the autoproxy, the advice, and the targets (Example 6-8).

Example 6-8. RentABike-servlet.xml
<bean name="profilerAround" 
   class="com.springbook.interceptors.ProfilingInterceptor"/>

<bean name="profilingAutoProxy" class="org.springframework.aop.framework.autoproxy.
BeanNameAutoProxyCreator">
        
   <property name="beanNames"><value>rentaBike*</value></property>
   <property name="interceptorNames">
      <list>
         <value>profilerAround</value>
      </list>
   </property>
</bean>

Now, when you run the application on std.out console, you can see that the application tells you when it enters or leaves a given method and how much time the method took to execute (Example 6-9).

Example 6-9. Standard out from running application
Entered toString
Method: toString took 0 ms.
Entered isSingleton
Method: isSingleton took 0 ms.
Entered getObject
Method: getObject took 0 ms.
Entered getObject
Method: getObject took 0 ms.
Entered getObject
Method: getObject took 0 ms.
Entered getObject
Method: getObject took 0 ms.
Entered getObject
Method: getObject took 0 ms.
Entered getObject
Method: getObject took 0 ms.
Entered saveBike
Method: saveBike took 31 ms.
Entered getBike
Method: getBike took 78 ms.
Entered deleteBike
Method: deleteBike took 62 ms.
Entered getBikes
Method: getBikes took 47 ms.

What just happened?

This example has two major differences from the previous one: we’re working on multiple beans, and we’re using a different type of advice. Let’s take a closer look.

Drill down on the interceptor itself first. You’ll notice that the MethodInterceptor advice, inside the invoke method, does some work, calls proceed, and then does more work. Drill down to the call to proceed in our interceptor. That method calls the next interceptor in the chain, or the target method if it’s at the last interceptor in the chain. You have a whole lot of flexibility here:

  • You can do some work before the proceed( ), like our caching of the start time of the method. That code will execute before the target method.

  • You can do some work after the proceed( ), but before the return. That code will execute after the target method.

  • You can decide not to call proceed( ). In this case, the target method will not execute.

  • Your code can fire an exception, changing the flow of control. In your exception, you may or may not decide to call proceed( ).

Otherwise, this kind of advice works just like the others. So which kind of advice should you use? Spring founder Rod Johnson suggests that you use the weakest type of advice that will work for you.

There’s another difference between this example and the last: the target. You’re proxying multiple targets. After Spring loads a context, it takes a post-processing pass through the beans of the context. In this pass, Spring applies this proxy definition to all of the beans in the context. It first decides if the bean matches the specified target. If it does, it creates the specified proxy.

Advising Exceptions

Often, you’ll want to attach a service to exception logic, rather than mainstream code. It’s especially important when exceptions force a major change in application flow, such as rolling back when an exception occurs, or automatically notifying administrators when some resource runs low.

Your application schedules bikes for a small store. You can use Spring’s exceptions to generate a message whenever something goes wrong in the application. For simplicity, you’ll send it to the console for now.

How do I do that?

The first job is to build an advisor. Use a simple class that implements the ThrowsAdvice interface, like in Example 6-10.

Example 6-10. ExceptionInterceptor.java
public class ExceptionInterceptor implements ThrowsAdvice {
    public void afterThrowing(Method m, Object[] args, 
           Object target, Exception ex) {

        System.out.println("Call to method " + m.getName( ) +
           " on class " + target.getClass( ).getName( ) + 
           " resulted in exception of type " + ex.getClass( ).getName( ));
        System.out.println("Exception message: " + ex.getMessage( ));
    }
}

Keep in mind that the ThrowsAdvice interface contains no method signatures; you can implement as many versions of afterThrowing as you want. Each must declare that last parameter as a subclass of Throwable. The other three parameters are optional.

Configure the parameters for the advice interceptor in the context. Next, you’ll configure the advice (note that Example 6-11 goes back to using the ProxyFactoryBean from the first part of this chapter, as opposed to an autoproxy, like the previous example).

Example 6-11. RentABike-servlet.xml
<bean id="exceptionInterceptor" 
   class="com.springbook.interceptors.ExceptionInterceptor"/>
<bean id="rentaBike" 
   class="org.springframework.aop.framework.ProxyFactoryBean">
  
   <property name="proxyInterfaces">
      <value>com.springbook.RentABike</value>
   </property>
   <property name="interceptorNames">
      <list>
         <value>exceptionInterceptor</value>
         <value>transactionInterceptor</value>
         <value>saveAdvisor</value>
         <value>rentaBikeTarget</value>
      </list>
   </property>
</bean>

In this case, notice that we’re still targeting methods. You’ll get notified whenever a serious exception occurs. To make sure it’s working, let’s try to remove a bike that doesn’t exist (Example 6-12).

Example 6-12. ControllerTest.java
public void testRemoveNonExistentBike( ) throws Exception {
   Bike b = new Bike(99, "me", "mine", 1, "1", 12.00, "good");
   store.deleteBike(b);
}

The console output in Example 6-13 is the result.

Example 6-13. Standard output from running application
Call to method deleteBike on class $Proxy0 resulted in exception of type 
org.springframework.orm.hibernate.HibernateSystemException

Exception message: Batch update row count wrong: 0; nested exception is 
net.sf.hibernate.HibernateException: Batch update row count wrong: 0

What just happened?

There’s no magic here. Spring uses the same mechanism, proxies, to intercept exception logic. We told the proxy to attach our interceptor to all of the methods that updated the database. Within our exception logic, we can decide what to do. In our case, we looked at the exception and decided which exceptions were important to us. We then simply dumped some information to the console, and returned control where it belonged: in this case, the façade method.

Testing a Service with Mocks

Testing AOP applications seems difficult, but we’ll give you a couple of techniques that simplify it. In this example, you’ll learn how to use mock objects with aspects.

Why do I care?

When you use an aspect, it’s often difficult to know if that aspect is working correctly. Since it’s not main-line code, the testing strategy is not always clear. By mocking your aspect, you can confirm that it’s fired when you expect it to be.

How do I do that?

We’re going to create a test case, and call a target method within the test case (Examples Example 6-14 and Example 6-15).

Example 6-14. InterceptMe.java
// simple intercept to proxy
public interface InterceptMe {
    void interceptThisMethod( );
}
Example 6-15. MockInterceptorTest.java
public class MockInterceptorTest extends TestCase implements InterceptMe {
    public void interceptThisMethod( ) {
           // empty method
    }
}

Then, in our actual test method, we’ll configure a proxy for the test case, and configure the proxy to intercept all the methods in our test interface (Example 6-16).

Example 6-16. ControllerTest.java
public void testLoggingInterceptor( ) throws Exception {
        Advice advice = new LoggingAround( );
        ProxyFactory proxyFactory = new ProxyFactory(this);
        proxyFactory.addAdvice(advice);
        InterceptMe target = (InterceptMe)proxyFactory.getProxy( );
        target.interceptThisMethod( );
}

Finally, we’ll use a mock object to record our expected behavior, and verify the behavior when we’re done. In this case, remember the definition of the LoggingAround interceptor (Example 6-17).

Example 6-17. LoggingAround.java
public class LoggingAround implements MethodInterceptor {
    private SessionFactory factory;

    public SessionFactory getFactory( ) {
        return factory;
    }

    public void setFactory(SessionFactory factory) {
        this.factory = factory;
    }

    private void logEvent(String methodName, String message) 
           throws Exception {
        Session s = null;
        LogEvent le = new LogEvent(methodName, new Date( ), message);
        try {
            s = factory.openSession( );
            s.save(le);
        } catch (Exception ex) {
            //log the exception
        } finally {
            s.close( );
        }
    }

    public Object invoke(MethodInvocation methodInvocation) 
           throws Throwable {
        logEvent(methodInvocation.getMethod( ).getName( ), "Entering call.");
        Object result = methodInvocation.proceed( );
        logEvent(methodInvocation.getMethod( ).getName( ), "Leaving call.");
        return result;
    }
}

We’re interested in verifying that it executes without actually having it write to the database. So we are going to provide a mock SessionFactory to mimic the behavior of Hibernate within the interceptor. Example 6-18 is the full test method with the mock objects added (note the programming injection of the mock SessionFactory into the interceptor).

Example 6-18. ControllerTest.java
public void testLoggingInterceptor( ) throws Exception {
        MockControl factoryControl = 
           MockControl.createControl(SessionFactory.class);
        MockControl sessionControl = 
           MockControl.createControl(Session.class);

        SessionFactory mockFactory = 
           (SessionFactory)factoryControl.getMock( );
        Session mockSession = (Session)sessionControl.getMock( );

        // this sequence will execute once before the target method... 
        mockFactory.openSession( );
        factoryControl.setReturnValue(mockSession);
        mockSession.save(new LogEvent( ));
        sessionControl.setMatcher(MockControl.ALWAYS_MATCHER);
        sessionControl.setReturnValue(null);
        mockSession.close( );
        sessionControl.setReturnValue(null);

        // and once after
        mockFactory.openSession( );
        factoryControl.setReturnValue(mockSession);
        mockSession.save(new LogEvent( ));
        sessionControl.setMatcher(MockControl.ALWAYS_MATCHER);
        sessionControl.setReturnValue(null);
        mockSession.close( );
        sessionControl.setReturnValue(null);

        factoryControl.replay( );
        sessionControl.replay( );

        Advice advice = new LoggingAround( );
        ((LoggingAround)advice).setFactory(mockFactory);
        ProxyFactory proxyFactory = new ProxyFactory(this);
        proxyFactory.addAdvice(advice);
        InterceptMe target = (InterceptMe)proxyFactory.getProxy( );
        target.interceptThisMethod( );

        factoryControl.verify( );
        sessionControl.verify( );
    }

What just happened?

When you think about it, our interceptors are just objects. We can mock APIs in them as easily as any other service. The trick is to take advantage of Spring’s light weight, and run the container in the test case. We just advised our test case.

Here, we’re just trying to make sure that the service works. We’re not trying to test any of the façade methods, only the advice. We attached our new service to our test case method. When the test case called the method, the advice fired, and we verified that in the mock object.

Testing a Service with Side Effects

Of course, you didn’t learn anything about the way our code used the services. To test the services, you’ll need to attach them to real code and exercise the code. In your test, you can merely look for a side effect.

When you use AOP, the business objects don’t always get exercised with basic out-of-container tests. You’ve got to run some tests in the container to make sure that transactions get committed or rolled back as they should, and that the security is behaving appropriately. Remember, the context is part of the program!

How do I do that?

You’ll simply exercise some code that’s using an AOP service and you’ll look for a known side-effect. Example 6-19 is a test case that causes an exception in the façade. We assert that the changes did not get committed.

Example 6-19. ControllerTest.java
public void testAddBadBike( ) throws Exception {
        int origCount = store.getBikes( ).size( );
        // collision on uniquely indexed serial number 11111
        Bike bike = new Bike(-1, "MyMan", "MyBike", 
           12, "11111", 12.00, "New");
        try {
            store.saveBike(bike);
        } catch (Exception ex) {
            assertEquals(origCount, store.getBikes( ).size( ));
            return;
        }
        fail("Should have errored out on bad insert.");
    }

What just happened?

You once again fired a test case from within the container. The test case called our façade directly. The façade did a database insert, but the bad data forced the transaction to roll back. Your test case made sure that the data did not get committed by doing a second read. In the next chapter, you’ll release the full power of AOP using some of Spring’s pre-built interceptors.

If you’ve never seen AOP or interceptors before, this chapter might have been challenging for you. If you want to learn more about aspect-oriented programming or design patterns for proxying interfaces, turn to these resources:

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.148.104.242