- 10 Best Practices for Jakarta EE Performance Optimization
- Performance Best Practice no. 1: Optimize database operations
- Performance Best Practice no. 2: Implement Caching
Jakarta EE applications can achieve better performance with a layered caching setup that combines client-side and server-side caching [1].
Caching Methods
Caching is a powerful technique used to improve the performance and scalability of applications. By storing frequently accessed data closer to where it’s needed, caching can significantly reduce latency and system load. Let’s take a look at some common caching levels and best practices:
Caching Level | Best Practice | Impact |
---|---|---|
Client-side | Browser cache for static assets | Reduces server requests |
Application | In-memory caching for data | Lowers database load |
Database | ResultSet caching | Cuts down query execution times |
Distributed | Cross-server cache sharing | Enhances scalability |
At the client-side, web browsers store static assets like images and stylesheets locally, reducing the need to re-download them on future visits. Most of the browsers try to guess which resources should be cached and for how long. However, it’s best practice to enable or disable caching by HTTP headers in responses.
At the application level, in-memory caches such as Redis or Memcached store frequently accessed data closer to the app, minimizing the need for repeated processing or database queries. On top of that, some of them like Hazelcast or Infinispan also support the standard JCache Java caching API and can even run in the same JVM.
Database caching involves storing the results of expensive or frequently run queries to avoid hitting the database engine repeatedly.
Distributed caching shares cached data across multiple servers, enabling consistent, fast access in high-traffic or scalable environments.
Client-side caching with GlassFish
By default, GlassFish doesn’t set any caching headers. It means that all static resources are cachable browsers should cache them for a while. For example, the HTTP Caching specification recommends caching resources that are older than 1 year. If you want to instruct browsers to cache all the static resources, including newer ones, you can do this with GlassFish by adding a servlet filter in your application. Just add a class similar to this into your application to cache the static resources with given extensions for 1 week:
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.time.Duration;
@WebFilter(urlPatterns = {"*.css", "*.js", "*.png", "*.jpg", "*.gif", "*.woff2", "*.svg", "*.html"})
public class CacheControlFilter implements Filter {
private static final long ONE_WEEK_IN_SECONDS = Duration.ofDays(7).toSeconds();
private static final long ONE_WEEK_IN_MILLIS = Duration.ofDays(7).toMillis();
@Override
public void doFilter(ServletRequest request, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) resp;
// Add Cache-Control headers
response.setHeader("Cache-Control", "public, max-age=" + ONE_WEEK_IN_SECONDS);
// Optional: Set Expires header for older HTTP/1.0 clients
long expiresInMillis = System.currentTimeMillis() + ONE_WEEK_IN_MILLIS;
response.setDateHeader("Expires", expiresInMillis);
chain.doFilter(request, response);
}
}
Application caching with GlassFish
GlassFish doesn’t provide any support for application caching out of the box. However, it’s easy to integrate Hazelcast into GlassFish as a JCA following this guide (Note that it uses a JCA adapter which depends on Java EE 8. To use it with a recent GlassFish, you’ll need to transform it, using this guide):
If you don’t need a JCA adapter and it’s enough to use Hazelcast directly, you can just add it to your application and use it with CDI, as described in this guide:
Hazelcast also supports distributing the cached data to other cluster members. To do that, you need to add configure Hazelcast to find other cluster instances or run Hazelcast cluster separately from GlassFish cluster, and connect to it from GlassFish applications in client-server mode.
You can use any other caching solution directly from your application, if you prefer a different solution than Hazelcast.
Database caching with GlassFish
You can get the best out-of-the-box caching experience with GlassFish if you access database using Jakarta Persistence (JPA). In GlassFish, this mechanism is by default provided by EclipseLink, which automatically caches all retrieved entities using the 1st-level cache and caches queries using the 2nd-level cache.
Both the 1st-level and 2nd-level caching in Jakarta Persistence is enabled automatically by default in GlassFish. There’s no need to enable it, but if the default behavior is not desired, you can disable it, either for all entities, or selectively for specific entities.
⚠ Warning: Note that the 2nd-level cache is preserved for the whole lifetime of the application (more specifically, of the persistence unit). Therefore it can become stale if the database is modified outside of the application. To avoid it, special treatment is needed, as described below.
To avoid keeping stale data, you need to make sure that the cache is evicted (cleaned) when the data in database is modified outside of the persistence unit context of the application. This is important in these main scenarios:
- Data is modified outside of the application, for example manually, by a DB script, or by another application, which directly accesses database.
- Data is modified by the same application but outside of the persistence unit context, e.g. via a direct JDBC statement.
- Data is modified by the same application running on a different instance of the same cluster
In the first case, the application does not know that the data in the data in the database is modified. A way to handle it, besides disabling the cache, which can have dramatic performance impact, is to implement a hook in the application which can be called from the outside. This hook can be, for example, a REST resource. It can trigger cleaning the cached data on an entity manager or the EM factory programmatically, using the Jakarta Persistence API:
em.getEntityManagerFactory().getCache().evictAll();
In the second case, the situation is similar. We just don’t need to call a hook externally, and can simply execute the cache eviction code after manipulating the database, e.g. after each JDBC statement execution or at the end of the transaction.
In the third case, we can set up automatic synchronization between clustered instances, so called cache coordination, so that instances automatically trigger cache eviction in other instances when they modify shared data. EclipseLink supports multiple cache coordination mechanisms. Currently, the easiest to set up with GlassFish, is the JMS mechanism. Since GlassFish provides a built-in JMS broker for each cluster instance, it’s very easy to set up. You just need:
To create a JMS topic in GlassFish JMS settings for coordination. The JMS topic should not be JTA enabled and should not have persistent messages
Configure EclipseLink to use the JMS mechanism
Configure EclipseLink to use the JMS topic via JNDI name and topic connection factory via JNDI name
This is an example configuration in a persistence.xml
file:
<property name="eclipselink.cache.coordination.protocol" value="jms" />
<property name="eclipselink.cache.coordination.jms.topic" value="jms/CoordinationTopic" />
<property name="eclipselink.cache.coordination.jms.factory" value="jms/CoordinationConnectionFactory" />
Next Steps
The next step is to actively identify areas within your applications where caching can provide the most benefit. Experiment with different caching strategies and carefully measure the performance improvements in your testing environments. Remember to also consider how you’ll manage cache invalidation to maintain data accuracy.
Building on this, our next article will shift focus to another vital area of performance optimization: tuning Jakarta EE server runtime settings. We’ll explore key configurations within the JVM and GlassFish server itself, such as JVM options and thread pool management, to further maximize your application’s speed and efficiency.