Task services encapsulate the non-agnostic automation logic of business processes and have a scope of execution that often corresponds to that of a business process instance. This chapter explores the design and implementation of task services with Java.
The service logic of a task service will often consist of controller logic, which composes entity and utility services as needed to fulfill functionality (Figure 10.1). Unlike agnostic services, which are generally built as standalone, shared IT resources, task services are modeled according to business process logic and designed around the execution of that logic via the composition of other services. The following case study demonstrates a simplified business process automated by a task service.
The messages defined for a task service are ideally derived from a canonical data model, although non-standard message structures can also be defined since task services are often used in and designed for one business process. A task service that needs to compose entity services that support canonical messages should utilize or map them appropriately in the implementation logic.
The service definition and message entity schema are generally defined in the same namespace.
The interaction between the task service and the underlying entity and utility services can be resource-intensive. An invocation of a task service can result in a large number of invocations on the underlying composed set of services. Simplifying the communication path between services can help maintain performance.
For optimal performance, task services should be hosted in a location close to where the composed services reside. However, because the types of services composed by task services are generally agnostic, they are used across business domains and may be part of multiple service compositions. Options for redundant implementations of shared agnostic services can be explored to support performance requirements, as per the Redundant Implementation pattern.
Task services need to retain the state data required for their task-centric processing across service invocations. State data can be represented by local, thread-specific variables in the code if the service is implemented in Java.
Additional mechanisms can be required to allow the service to store state information temporarily in a persistent store (as per the State Repository pattern) or a dedicated state management utility service (as per the Stateful Services pattern) if the handled state becomes too large.
The upcoming sections delve deeper into the architectural and programming details of task service implementations.
The implementation logic for Java task services should be independent of the underlying technologies and code that use low-level Java APIs. This can be accomplished by abstracting lower level API functions into the utility service layer.
As an example, assume that an e-mail should be sent as part of a task service’s execution. The JavaMail API offers e-mail functionality that the service logic can utilize directly.
Alternatively, the service logic can delegate e-mail processing to an E-Mail utility service, further decoupling the task service from any dependency on underlying runtime features. The utility service can handle the details of accessing mail servers and the formatting and parsing of e-mail content.
Building task service logic can require the generation of code skeletons from service logic can require the generation of code skeletons from the interface, using the wsgen tool in the JAX-WS or implementing the logic for REST task services in a POJO with the JAX-RS annotations. Similarly, all services composed by the task service are available in WSDL format for Web services or as POJOs. When starting from WSDL, the appropriate Java interfaces and proxy classes can be generated using the same tool.
Sometimes the designed WSDL service interface document or resource representation format, such as the XML schema, is inappropriate for implementation in Java. For example, many Java classes will be generated using a data binding tool, such as the JAXB, from an interface or representation using schemas with large numbers of defined complex types.
The generation of too many classes can often be resolved by adjusting the schemas, but in many cases, the schemas are reused across many services and cannot easily be modified. Certain process automation environments can dictate the structure of schemas in a way that results in the same inflation of generated Java classes.
Task services compose entity services to retrieve business-centric data required for business task processing. The result of an invocation of one entity service operation may be used for the invocation of another, so that data from various sources can be aggregated. To avoid large data caches during invocation, ensure entity services are used to limit the amount of returned data. Efforts should be made to restrict query parameters to prevent excessively generic queries.
Data can be cached in the local heap. Ensure the data is stored in local variables so that it is scoped to only the current thread, although the multithreaded nature of Web service deployments already reinforces data storage in local variables. Data that cannot be stored in the local heap, such as amounts of data beyond the size of memory available, must be outsourced by storing it directly in a local file or database.
Simultaneously, ensure outdated data is cleared. Just as with other low-level APIs in a task service implementation, delegating the handling of local temporary caches to a utility service is advised. This type of cache service can be invoked locally as a Java class and exist in the same process as the actual service implementation, because of the potential performance impact of moving large amounts of data between processes.
The behavior of a task service often depends on the identity of the invoking caller, which needs to be transferred from the service consumer to the service if such behavior must be implemented in the service logic. If the service logic is implemented in an EJB, the caller’s identity can be discovered using the getCallerPrincipal()
method on the EJB context. If the service is implemented in a JAX-RS resource class, the javax.ws.rs.core.SecurityContext
interface can be used to obtain security-related information, such as user principals and user role information.
Incoming requests carry user credentials that are carried within the scope of the application server or container, translated into the Java security context java.security.Principal
. Explicitly passing user identity information along with a request is often unnecessary if a proper security model has been established.
If a full Java EE environment is one of the deployment options for the task service, implementing the service logic inside a stateless session EJB is recommended. EJBs are stateless and offer a set of features and characteristics useful for task services without requiring any special coding, such as concurrent access, instance pooling, and transactional settings.
Implementing a task service as a stateless session bean allows for accessibility as a Web service via RMI/IIOP as a remote EJB, and locally via its local interface. A stateless session bean that has a Web service interface as well as a remote and local EJB interface can be deployed once and accessed in all three ways.
Multiple deployment options offer potential service consumers various choices based on performance requirements, available platforms, and other considerations. However, using local EJB interfaces requires that both service consumer and service are bundled together in a single deployment artifact and deployed on the same application server. Tight coupling between the service consumer and services should be avoided wherever possible.
For example, both service consumer and service must be redeployed together with the service consumer code if maintenance must be carried out on the service logic. The coupling is not as tight when using remote EJB interfaces, but the service consumer must still use a number of Java artifacts to provide access to the EJB. Using a Web-based service interface offers the loosest degree of coupling between service consumer and service. Figure 10.4 summarizes the different access options for service consumers with a service implemented as a stateless session EJB.
Since Java EE 6 and EJB 3.1, REST services that are stateless session beans or singleton beans can be marked as JAX-RS resources that can leverage the features of the EJB container. If a service is deployed in a pre-Java EE 6 container, a regular JAX-RS annotated POJO can be used to model a REST resource and the transaction handling delegated to a session EJB. When deploying services in a servlet or Java EE 6 or 7 Web Profile containers, the degree of support for transaction handling can vary from one implementation to another. Therefore, investigate the extent of support to determine whether any special application-level transaction handling is required.
A task service often composes one or more entity services to retrieve required data stored in a persistent and transactional data store. Changes to data must be made within the scope of a transaction, and multiple changes to data stored in different data stores must be made within the same transaction to avoid leaving the data stores in an inconsistent state. The task service implementation often determines the boundaries of transactions.
The following sections highlight issues pertaining to the creation of task services as SOAP-based Web services and REST services.
Deploying a task service as a remote Java component, such as an EJB, tightly couples the service consumer and service. The process execution environment can be based on non-Java technology, if required, as Web services can be problematic in cases where a non-functional context must be preserved across service invocations.
The RMI/IIOP protocol used between Java EE application server processes automatically promotes context information about transactions or identity. This is not the case when using Web services, unless advanced WS-* standards (such as WS-Security and WS-AtomicTransaction) are used. Support for these standards is required on both the service consumer and service sides.
The following NovoBank case study example demonstrates the use of JAX-WS APIs for the creation of a SOAP-based task Web service.
Modeling any one of the entity services composed by a task service as a resource may not useful. As discussed in Chapter 9, an aggregate resource can be unsuitable for handling updates across multiple entities. In this case, the solution is to model the action itself as a controller resource. The controller hides the complex coordination details of the task service, gathers the response from multiple actions, and returns the results of the execution to the caller. However, two issues arise when modeling the action as a REST controller resource:
• Multiple entities could be created or updated from a task service. How can the results be communicated in a REST-based way back to the service consumer?
• Depending on the results of the processing, a number of different subsequent actions may be performed by the service consumer. How can the service consumer be guided through the appropriate sequence of subsequent actions?
The following case study example for SmartCredit’s Credit Application Processing service illustrates methods to resolve the issues involved in modeling the action itself as a controller resource.
Unit testing for task services is challenging because of the number of dependencies they have on other services. As a result, testing the task service implementation can require a relatively large environment. To reduce task service dependency on other services which may or may not be available at the time of implementation, consider generating local or remote stubs used in place of the actual services invoked.
The service interface definition can be a WSDL document tooled to generate a Java interface for stubs. A simple Java class that implements this interface and returns some meaningful hardcoded data can be developed. In the service logic, an instance of a proxy to the target service is often retrieved via the standard JAX-WS mechanisms by utilizing the generated javax.xml.ws.Service
instance class.
For unit testing purposes, the call can be temporarily removed to the getPort()
method to retrieve a reference to the service proxy. The local implementation that can be locally invoked can instead be used to prevent creation and parsing of XML and cross-process invocations. This allows developers to focus on the actual business logic within the service implementation for testing purposes.
The implementations of composed services can be wrapped into a Web service layer but deployed on a server similar to the one where the task service logic is tested. The JAX-WS code can be reinstated for retrieving a reference to the service proxy class but set to a URL of the locally deployed service, thereby avoiding dependencies on existing deployments of target services and allowing the code to be tested for remote invocation of the composed services through the JAX-WS layer.
In the integration testing phase, all instances of invoking test stub services can be replaced by invocations of the target services. Chapter 11 details how to manage endpoint addresses of composed services across development phases. For testing a REST task service, as with any other REST service, any HTTP library can be used for making the appropriate HTTP requests. For example, the curl
utility can be used to issue a GET request for a credit application, as seen in Example 10.17.
curl -H "Accept: application/xml" -X GET
"http://smartcredit.com/creditapp/xw5f45892"
Task services have no specific packaging considerations beyond the resolution of dependencies towards composed services.