J2EE Design Strategies That Boost Performance

J2EE Design Strategies That Boost Performance

J2EE takes the hassle out of distributed programming, but there’s a catch. Although you no longer have to contend with such low-level issues as threading, synchronization, or persistence, you also relinquish control of key architectural considerations such as load balancing and scaling. Leaving these tasks?which strongly impact performance?to the EJB container can lead to performance problems, because ultimately no third-party software can know your application as well as you do.

Performance problems manifest themselves in many ways:

  • JSPs take too long to load.
  • Search screens take minutes to show results or never return at all.
  • The user interface does not respond, and users click furiously but to no discernible effect.
  • At peak times, the server fails unexpectedly due to “out of memory” errors, locked database tables, and connection timeouts.
  • Your previously reliable nightly batch processes become strangely fragile and take longer to run.

The day-to-day operation of the business is being impacted and the extra hardware you want isn’t going to arrive anytime soon. You need to diagnose the problem and implement the fixes. A few basic techniques?DBMS optimization, caching, threading, fine-grained transaction control, deferred processing, and EJB restructuring?can help you take back control of your application to meet your performance targets.

Design with Performance in Mind
Unless you consider and factor performance into your user interfaces, designs, and development processes up front (including testing early and often), you’ll likely hit problems sooner or later. The key to good performance is to understand your performance goals up front. Understand the number of concurrent users, target response times, usage patterns, and data growth. Know how your application is going to be used: which parts of the system are session-based and which are non-session or batch-based. That way you can structure your architecture to meet your needs.

Focus on What’s Important to the Business
Sometimes the performance aspects of a system are left to the end of the project. Developers just don’t have time to consider response times when speed of delivery and functionality are the pressing goals. Performance bottlenecks must be systematically identified and solved, which is no light task. The later in the development life cycle you address them, the more expensive they become to fix.

Small optimizations (like turning logging down) can yield huge gains, whereas others can have minuscule effects. The key to tackling bottlenecks is to prioritize and focus on the areas of your code that will yield the biggest return for your time investment. Focus on frequently used parts of the code, the parts critical to your business. Gather accurate performance metrics and use repeatable test cases to validate against your performance targets.

Performance Patterns and Techniques
You can scale your system to satisfy an increased load by using vertical and horizontal scaling techniques (software and hardware), but not always. Sometimes you can’t use these techniques because either the budget is tight, time is limited, or your system has inherent architectural limitations. In lieu of adding more hardware, distributing your code, or adding more servers, you can use the following server-side strategies to improve the performance of a single J2EE/EJB application.

Database I/O Optimization

  • Poor DBMS performance can arise from a mismatch between the data-model, entity-bean design, and its usage. If your most frequent user request joins 10 different tables and returns five entities of which you use only one, then this is a design flaw. Align your DBMS access to support your usage patterns. Write a query to return only the one piece of data that you need.
  • Search queries that use finders can take a long time to run. Not only do they execute the finder query, but they also make subsequent calls to the DBMS to load entity beans. It’s more efficient to call the DBMS with one bulk query than it is to issue a series of smaller requests.
    Consolidate your search queries into a single JDBC call using a DataAccessObject (DAO) or FastLaneReader (Marinescu/J2EE Blueprints).
  • If your query results are large, however, you may encounter problems when you try loading it all into memory in one hit. If the client paginates the results, consider restricting the number of rows returned to a total closer to your page size. You can always fetch more rows on demand.
  • Use built-in database features that may help reduce query times, such as stored procedures, indexes, views, and table caches.
  • Caching

  • Within a given user transaction, the same data may be requested multiple times. Try to reduce the number of redundant reads. Either pass the information around as method parameters or consider caching it on the session or thread context. Data that changes infrequently, such as meta data or configuration data, is good for caching. You can load the cache once at startup or on demand.
  • Place your server caches strategically inside your facades to make them transparent to the caller.
  • Transaction Control

  • Long-lived transactions can lead to connection timeouts, sharp memory increases, and DBMS lock contention. Your session beans control transactions. Break up extremely long-lived transactions into multiple shorter, more-reliable chunks.
  • Chaining together many short-lived transactions can also be slow. Widen your transaction boundaries. Increase throughput and efficiency by manipulating the length of your transaction so it performs many operations at once rather than one at a time.
  • Establish general-purpose session bean controllers to control your transactions. These can be independent of your business logic. Use your beans to adjust the number of records processed in one transaction so that you get optimum throughput for your request.
  • Threading

  • Batch processes written with EJBs can run very slowly because developers often build single-threaded batch jobs without considering transactions. A process that executes inside one very long-lived transaction (that could take hours or days) is fragile. If something goes wrong, the entire job gets rolled back and you have to start again from scratch. If your job executes as many small transactions chained together inside a loop, then your job will be more reliable. The throughput will be low, however.
  • The container controls server-side threads. Application code cannot create its own threads inside the EJB container, so it cannot take advantage of threading directly. Typically, a container-managed thread pool dispatches and services incoming server requests. This model works well for session-oriented usage. The container load balances across competing requests.
    However, for batch processes, the goal is to achieve high throughput so the job finishes as quickly as possible. Parallel processing can aid this. Spawn threads outside the container and divvy up the load into small units of work that are capable of being executed concurrently. Use threads to spread the load. Don’t spawn more threads than the server can handle. Keep the number of client threads to fewer than the containers thread pool limit.
  • Aggressive threading can reveal locking issues and non-thread-safe code inside the server. Structure your components so that they are thread-safe and can work concurrently, avoiding DBMS contention. Divide your units of work up so that they operate over different data sets.
  • Deferred Processing

  • How real-time are your requirements? Not every component has to respond in real-time. If your on-line response time is slow, reduce the amount of work performed during the request and defer expensive or non-essential functions to later. You can store work temporarily in holding tables or execute it asynchronously using JMS.
  • Components that don’t have strict real-time requirements, such as data-extraction programs, collation and sorting routines, file import/export, etc., can be performed off-line as batch processes.
  • EJB Restructuring

  • Every bean you write comes with built-in overhead. Many beans add up over the lifetime of a request. When a request is made to a session bean, the container has to locate and allocate resources (threads, objects, and connections), transfer context information, and serialize parameters?which takes time.
    Not every business component has to be a bean. Write your core components as plain old Java objects (POJOs). Design your core business logic and domain model independently of your session beans. That way, your core components are mobile, flexible, and reusable.
  • Think of your session beans in terms of deployment not development. Put your session beans in to manage your remote access points and control your transactions. Wrap them around your existing core business components.
  • Structure your session beans as general-purpose transaction controllers with extensible command-like remote interfaces. Delegate the business logic to your core components. Use session beans and remote interfaces sparingly to minimize request overheads and streamline your code.
  • Database I/O Optimization Implementation?Pure JDBC for Read-only Access
    Entity beans provide an abstraction layer between the higher-level business components and the database. When a developer makes a call to a method on a bean, he or she doesn’t see what happens under the covers because it’s transparent. The developer can assemble large objects or data structures using many entity calls. He or she can be oblivious to the expensive low-level database I/O taking place. One way to alleviate this bottleneck is to replace your critical sections with specialized pieces of pure JDBC code that are optimized for the task. DataAccessObjects (DAOs) and FastLaneReaders (Marinescu/J2EE Blueprints) are common techniques for accelerating reads. These patterns give you fine-grained control over your queries for efficient read access. A DAO can be used to consolidate many DBMS requests made by an entity bean (finder + loads) into a single JDBC call. DAOs are useful for supporting search screens or requests over data that span multiple beans and/or database tables.

    The following are recommended practices for DAO:

    • Use your DAO to query for data directly from the DBMS rather than via your entity beans. Session beans and other components can call your DAO directly.
    • Design your DAO so that it can be tailored to return only what you need.
    • Design your DAO so that it can limit the number of rows returned.
    • Use finally clauses to ensure that all JDBC resources are closed when you’re finished with them. This ensures that you don’t hog or consume connections and cursors.
    • Query against DBMS views to make your DAOs more reusable and portable. If you need to make a change to a query, make a change to the view instead.

    With a DAO you can take advantage directly of the features provided by the JDBC API, including:

    • PreparedStatements to pre-compile frequently executed SQL statements
    • Batch methods on the Statement class (addBatch() and exectuteBatch()) to batch up multiple SQL calls into one hit to the database
    • Row limiting (The Statement and ResultSet classes provide methods (setMaxRows() and setFetchSize()) to limit the number of rows returned and set hints for ResultSet fetch sizes.)

    See Listing 1: A Simple DAO for executing SQL against a view and limiting the number of rows returned in a ResultSet.

    As part of your DAO design, you must decide in which form the data should be returned and how it should be converted. In some cases, you may want to return loosely typed collections such as Lists or Maps, and in other cases you may want to immediately convert ResultSet data into strongly typed Java objects relevant to your system. One way to handle this at the DAO level is to use a helper ResultSetConverter interface. This interface is responsible for converting RowSets into strongly typed application object types or collections. The DAO uses it to automatically convert ResultSet data into your target object(s):

    public interface ResultSetConverter {	public Object toObject(ResultSet rs) throws Exception;}

    Create a simple ResultSet to Map Converter class by implementing the ResultSetConverter interface. Inside the toObject() method, pull the data from the result set and place it into a map of column name/value pairs. The map is returned to the DAO to be passed back to the DAO caller:

    class MapConverter implements ResultSetConverter {public Object toObject(ResultSet rs) throws Exception {	Map map = new HashMap();	ResultSetMetaData meta = rs.getMetaData();	// Load ResultSet into map by column name	int numberOfColumns = meta.getColumnCount();	for (int i = 1; i <= numberOfColumns; ++i) {		String name = meta.getColumnName(i);		Object value = rs.getObject(i);		// place into map		map.put(name, value);	}	return map;}}

    To use your DAO and converter, acquire a DAO instance and invoke the query() method to execute your SQL. Use the rowLimit parameter to limit the number of rows returned and pass in the converter class for the DAO to use.

    DAO dao = DAO.get();		// Create our own converter for getting the first column as a StringResultSetConverter myConverter = new ResultSetConverter() {	public Object toObject(ResultSet rs) throws Exception 	{		return rs.getString(1);	}};// Execute a query against a VIEW,
    limit the number of rows returned to 10 and use myConverter to convert the resultsList data = dao.query("myView", 10, myConverter);// Do something with the data, ship to the JSP etc.

    The J2EE Performance-tuning Trade-off
    Experienced practitioners know that when addressing J2EE application performance issues, there are no silver bullets. Performance tuning is a trade-off between architecture concerns, such as flexibility and maintainability. Performance increases are won by combining different techniques, patterns, and strategies.

    And if all else fails, you can hope that that extra-fast machine you ordered turns up sooner rather than later. 🙂


    About Our Editorial Process

    At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

    See our full editorial policy.

    About Our Journalist