Dynamic Proxies in ABAP
With version 1.3 dynamic proxy classes were introduced into Java’s reflection package. An instance of a dynamic proxy class implements a list of interfaces specified at runtime and dispatches method invocations through one of these interfaces to another object implementing the generic InvocationHandler interface:
Dynamic proxy classes have proved to be quite handy, especially when it comes to dynamically enhancing the behaviour of objects. For this purpose, dynamic proxies can be transparently interposed between the client of an interface and the implementing object, intercepting invocations of its methods and offering hooks for additional processing both before and after the call has been forwarded to the original implementation, thus providing a mechanism comparable to advicesin Aspect Oriented Programming (AOP).
As an illustration of the advantages such an approach offers, consider the following (unfortunately rather tired) example: Suppose you wanted to trace all calls of some service exposed through an interface. Using a generic invocation handler that logs all calls it intercepts before delegating them to the original implementation, dynamic proxy classes allow you to solve your problem in a general and transparent manner. In other circumstances, they can also act as a lightweight alternative to code generation, e. g. to provide mock objects needed for unit testing.
Dynamic proxy classes have been leveraged in a number of popular Java frameworks, e. g. Spring and EasyMock, to name just two. In this series of blogs I will present a small framework based on dynamic code generation that duplicates most of the functionality of Java’s dynamic proxies in ABAP, so those of us stuck on the dark side of SAP need not miss out on all of the fun.
Let us begin with an outline of the general concept behind the framework, leaving out the pesky details (like e. g. how to deal with interfaces that define static methods or events) for the moment.
Our goal is to generate a class that implements a list of interfaces specified at runtime. The basic operation of this dynamic proxy class consists of encoding the invocations of its methods and forwarding them to another object through a generic interface. Specifically, the proxy needs to be able to pass parameters and return values between its own caller and the invocation handler it calls in turn.
Fortunately, ABAP with its strong support for dynamic programming already provides a solution for a similar problem: A mechanism to dynamically call arbitrary methods. The ABAP documentation informs us about the corresponding syntax:
CALL METHOD meth_identifier PARAMETER-TABLE ptab EXCEPTION-TABLE etab
where ptab and etab are of typeABAP_PARMBIND_TAB and ABAP_EXCPBIND_TAB respectively. As an example illustrating how this works, let us retrieve the type information for class CL_GUI_FRONTEND_SERVICES through a dynamic method call:
type-pools ABAP. data LV_METH_NAME type STRING value 'DESCRIBE_BY_NAME'. data LV_TYPE_NAME type STRING value 'CL_GUI_FRONTEND_SERVICES'. data LR_DESCR type ref to CL_ABAP_TYPEDESCR. data LS_PARAM type ABAP_PARMBIND. data LT_PARAMS type ABAP_PARMBIND_TAB. data LS_EXCP type ABAP_EXCPBIND. data LT_EXCPS type ABAP_EXCPBIND_TAB. LS_PARAM-NAME = 'P_NAME'. LS_PARAM-KIND = CL_ABAP_OBJECTDESCR=>EXPORTING. get reference of LV_TYPE_NAME into LS_PARAM-VALUE. insert LS_PARAM into table LT_PARAMS. LS_PARAM-NAME = 'P_DESCR_REF'. LS_PARAM-KIND = CL_ABAP_OBJECTDESCR=>RECEIVING. get reference of LR_DESCR into LS_PARAM-VALUE. insert LS_PARAM into table LT_PARAMS. LS_EXCP-NAME = 'TYPE_NOT_FOUND'. LS_EXCP-VALUE = 1. insert LS_EXCP into table LT_EXCPS. LS_EXCP-NAME = 'OTHERS'. LS_EXCP-VALUE = 2. insert LS_EXCP into table LT_EXCPS. call method CL_ABAP_OBJECTDESCR=>(LV_METH_NAME) parameter-table LT_PARAMS exception-table LT_EXCPS. case SY-SUBRC. when 1. message 'Type not found' type 'E'. when 2. message 'Unknown error' type 'E'. endcase.
That was not too complicated. So using the syntax for dynamic method calls as a blueprint, our version of Java’s InvocationHandler interface takes on the form:
interface ZIF_DP_INVOCATION_HANDLER public. methods INVOKE importing !IR_PROXY type ref to ZIF_DP_PROXY optional !IV_METHOD_NAME type ABAP_METHNAME !IT_EXCEPTIONS type ABAP_EXCPBIND_TAB optional exporting !EV_RC type SYSUBRC changing !CT_PARAMETERS type ABAP_PARMBIND_TAB optional raising CX_STATIC_CHECK CX_DYNAMIC_CHECK. endinterface.
Following Java’s lead again, we introduced parameters IR_PROXY and IV_METHOD_NAME to pass the calling dynamic proxy instance and the name of the originally invoked method to the invocation handler. Parameters CT_PARAMETERS and IT_EXCEPTIONS contain the call’s parameters and the return codes supplied by the caller for any old style exceptions (returned via EV_RC) respectively. In addition, we declare CX_STATIC_CHECK and CX_DYNAMIC_CHECK in the raising clause to make sure we can throw arbitrary class-based exceptions to the caller.
The dynamic proxy class’s job of translating its method invocations into a form consistent with ZIF_DP_INVOCATION_HANDLER~INVOKE involves ABAP code very similar to that shown above preparing the parameters for a dynamic method call. The proxy will need to provide such code for each method of the interfaces it implements. To generate this code we will follow the agenda outlined below:
- Use ABAP’s classes for Runtime Type-Information (RTTI) to retrieve a description of the relevant interfaces.
- Get the canonical XML representation of said description.
- Call an XSL transformation with this XML document to generate the source code for the dynamic proxy class.
- Make the code executable either by putting it in a transient subroutine pool or by storing it permanently as a report in the database.
Of course, we also need to decide on an appropriate API for our little framework. To allow for some measure of flexibility, we provide distinct interfaces for constructing a dynamic proxy class implementation:
interface ZIF_DP_PROXY_MANAGER public. methods GET_PROXY_FACTORY importing !IT_INTFNAMES type INTFNAME_TAB returning VALUE(RR_FACTORY) type ref to ZIF_DP_PROXY_FACTORY raising ZCX_DP_EXCEPTION. endinterface.
and creating instances of it:
interface ZIF_DP_PROXY_FACTORY public. methods CREATE_PROXY importing !IR_INVOCATION_HANDLER type ref to ZIF_DP_INVOCATION_HANDLER returning VALUE(RR_PROXY) type ref to ZIF_DP_PROXY raising ZCX_DP_EXCEPTION. endinterface.
Calling ZIF_DP_PROXY_MANAGER~GET_PROXY_FACTORY with a list of interface names in parameter IT_INTERFACES will yield an instance of ZIF_DP_PROXY_FACTORY. Via the latter’s method CREATE_PROXY the client can then instantiate a proxy that forwards invocations of its methods to the invocation handler instance associated with it via parameter IR_INVOCATION_HANDLER. To report any problems the framework encounters we will use appropriate exception classes derived from ZCX_DP_EXCEPTION.
ABAP Strikes Back
So far, everything is going extremely well, to quote a favourite movie of mine. However, there exist a few differences between Java and ABAP that force us to go to some extra length to reach our goal. ABAP Objects supports events at the language level and therefore makes the events an interface may raise part of its definition. Consequently, for our dynamic proxy to be a fully functional replacement of a conventional, compile-time based implementation, we need to figure out a mechanism through which the invocation handler can trigger these events in a generic fashion. In other words, we want our dynamic proxy to expose the following interface:
interface ZIF_DP_PROXY public. methods RAISE_EVENT importing !IV_EVENT_NAME type ABAP_EVNTNAME changing !CT_PARAMETERS type ABAP_PARMBIND_TAB raising ZCX_DP_EXCEPTION. endinterface.
Calling method ZIF_DP_PROXY~RAISE_EVENT will cause the proxy to raise the event named in IV_EVENT_NAME with the parameters given in CT_PARAMETERS. Our proxy class will need to implement ZIF_DP_PROXY in addition to the other, dynamically specified interfaces.
Another difference between Java and ABAP Objects is the latter’s ability to define static methods within an interface. Any such definition resolves to a static method in a class implementing the interface. Since it is impossible to access instance variables from within a static method, our proxy class will not be able to forward invocations of the method to its per-instance invocation handler, but requires a dedicated class attribute to hold the reference of a per-class invocation handler instance. We account for this by enhancing the definition of the proxy’s internal service interfaceZIF_DP_PROXY accordingly:
interface ZIF_DP_PROXY public. class-data MR_STATIC_HANDLER type ref to ZIF_DP_INVOCATION_HANDLER. methods RAISE_EVENT importing !IV_EVENT_NAME type ABAP_EVNTNAME changing !CT_PARAMETERS type ABAP_PARMBIND_TAB raising ZCX_DP_EXCEPTION. class-methods SET_STATIC_HANDLER importing !IR_STATIC_HANDLER type ref to ZIF_DP_INVOCATION_HANDLER. endinterface.
Furthermore, we also extend our API by adding the additional parameter IR_STATIC_HANDLER to ZIF_DP_PROXY_MANAGER~GET_PROXY_FACTORY, so that our clients can specify the per-class invocation handler when they retrieve the proxy factory:
methods GET_PROXY_FACTORY importing !IT_INTFNAMES type INTFNAME_TAB !IR_STATIC_HANDLER type ref to ZIF_DP_INVOCATION_HANDLER returning VALUE(RR_FACTORY) type ref to ZIF_DP_PROXY_FACTORY raising ZCX_DP_EXCEPTION.
The third ABAP-specific language construct we concern ourselves with are static events. It would be nice if we were able to raise an interface’s static events without the necessity of first getting hold of an implementing instance. To this end, we add RAISE_STATIC_EVENT as static method to interface ZIF_DP_PROXY:
class-methods RAISE_STATIC_EVENT importing !IV_EVENT_NAME type ABAP_EVNTNAME changing !CT_PARAMETERS type ABAP_PARMBIND_TAB raising ZCX_DP_EXCEPTION.
and also include it as instance method in ZIF_DP_PROXY_FACTORY. Putting all changes together, our extended API now looks like this:
interface ZIF_DP_PROXY_MANAGER public. methods GET_PROXY_FACTORY importing !IT_INTFNAMES type INTFNAME_TAB !IR_STATIC_HANDLER type ref to ZIF_DP_INVOCATION_HANDLER returning VALUE(RR_FACTORY) type ref to ZIF_DP_PROXY_FACTORY raising ZCX_DP_EXCEPTION. endinterface. interface ZIF_DP_PROXY_FACTORY public. methods CREATE_PROXY importing !IR_INVOCATION_HANDLER type ref to ZIF_DP_INVOCATION_HANDLER returning VALUE(RR_PROXY) type ref to ZIF_DP_PROXY raising ZCX_DP_EXCEPTION. methods RAISE_STATIC_EVENT importing !IV_EVENT_NAME type ABAP_EVNTNAME changing !CT_PARAMETERS type ABAP_PARMBIND_TAB raising ZCX_DP_EXCEPTION. endinterface.
whereas the internal interface ZIF_DP_PROXY which the dynamic proxy class exposes towards the framework takes on the form:
interface ZIF_DP_PROXY public. class-data MR_STATIC_HANDLER type ref to ZIF_DP_INVOCATION_HANDLER. methods RAISE_EVENT importing !IV_EVENT_NAME type ABAP_EVNTNAME changing !CT_PARAMETERS type ABAP_PARMBIND_TAB raising ZCX_DP_EXCEPTION. class-methods RAISE_STATIC_EVENT importing !IV_EVENT_NAME type ABAP_EVNTNAME changing !CT_PARAMETERS type ABAP_PARMBIND_TAB raising ZCX_DP_EXCEPTION. class-methods SET_STATIC_HANDLER importing !IR_STATIC_HANDLER type ref to ZIF_DP_INVOCATION_HANDLER. endinterface.
We have now talked at length how to deal with the methods and events of an interface. But what about the attributes it might define? They are automatically part of every class that implements the interface and therefore accessible by the client. Unfortunately, I can not think of a way to intercept the access to them and redirect it appropriately, hence I consider them a lost cause for our purposes. Just one more reason (besides data encapsulation and implementation hiding principles) why access to an object’s attributes should only be granted through dedicated accessor methods.
So much for this overview, in the next instalment we will get started with the first point on the agenda we made earlier.