Tuesday, September 11, 2018

Spring Boot 2 + Thymeleaf - Versioning static assets for client side cache busting

In a post written some time ago, I showed how you can prevent caching of static resources (javascript and css) in a legacy Spring MVC application. Recently I needed to use the same technique to prevent browser side caching of javascript and css files in a Spring Boot 2 project using Thymeleaf as the view layer.

Purpose of this blog post


This blog post is about sharing a simple way to make your Spring Boot application serve versioned static resources like javascript and css files so that client side caching can be overcome when your static resources change.
Typically browsers download javascript and css files and then refer to them from the local cache. This is okay as long as you do not change the contents of your files. If you do make changes but you keep the same name then the browser will not know about the changes and use the same version from its local cache. Some approaches to defeat the cache are to add add a distinguishing query parameter (like myfile.css?v=1.1) or placing the files in a new subdirectory (like /js/v2/myfile.js) and then updating all the places in the HTML files where these are included. This is tedious and error prone to manually update the version query parameter or rename the file location in all the pages that it is included.

Fortunately Spring allows us to take care of this with minimal effort. Let us see how.

What is used to build the example in this blog post?

This example applies to a Spring Boot 2 application with Thymeleaf as the view layer. I am assuming that you have a standard Spring Boot 2 app with Thymeleaf. I am sure that this can be adapted to any other view technologies barring one as mentioned in the note below.

Before you start, I recommend you refer to the official documentation about serving static resources for further clarity.

Please note that if you are using Spring WebFlux then you cannot use this approach. See the official documentation here for more details.

Step 1: Adding entries to your application.properties (or application.yml) file

You need to add the following three entries in your application.properties file. What these mean is explained very clearly on the Spring documentation here in the Spring Resources Handling section.

File: application.properties


#########################################################
#Adding ResourceUrlEncodingFilter i.e. to version static 
#js and css files to prevent caching by browser
#########################################################

#Whether to enable the Spring Resource Handling chain. 
#By default, disabled unless at least one strategy has 
#been enabled.
spring.resources.chain.enabled=true

# Whether to enable the content Version Strategy.
spring.resources.chain.strategy.content.enabled=true

#Comma-separated list of patterns to apply to the content 
#Version Strategy i.e. where are your js and css files
spring.resources.chain.strategy.content.paths=/js/**,/css/**


Step 2: Define a ResourceUrlEncodingFilter bean

You need to create a ResourceUrlEncodingFilter bean and register it in your application's filter chain. With Spring Boot, it is very simple. Just create a MVC configuration and instantiate the bean there. For example, the following is all you need.

File: CacheBusterMVCConfig.java


@Component
public class CacheBusterMVCConfig implements WebMvcConfigurer {

    /**
     * Define the ResourceUrlEncodingFilter as a bean. 
     * Add the <code>@ConditionalOnEnabledResourceChain</code> 
     * to ensure that the resource chain property 
     * <code>spring.resources.chain.enabled=true</code> 
     * is set to true in the <code>application.properties</code> file.
     * 
     * @return
     */
    @Bean
    @ConditionalOnEnabledResourceChain
    public ResourceUrlEncodingFilter resourceUrlEncodingFilter() {
        return new ResourceUrlEncodingFilter();
    }
}


Results

If you download and run the project, you should see the screen as shown below.


Resources

The example project has been uploaded to Github here. If you like it, please star my repo.

Happy coding :)

Friday, May 25, 2018

Adding tenants without application restart in SaaS style multi-tenant web app with Spring Boot 2 and Spring Security 5

In my previous posts (part 1, part 2) about Saas style multi-tenant web applications, the focus was on how multi-tenancy is achieved.  In this post I will share how tenants can be added without restarting the application.


Update 4th Sep 2018

Many readers of this blog post have emailed me saying that they run the code but cannot login. This is because they have not populated the user table with username and password for a user nor have they created the necessary databases and schema. In many cases, the readers are not familiar with Spring Security. I request that you please read up on Spring Security first. Here is an excellent example from the official Spring documentation site.

Purpose of this blog post

In my previous posts, I showed how to read the details of the tenant databases from the application.yml file. The focus was on how to set up SaaS style database per tenant multitenancy.

In this post, I will show you how you can add the tenant information in a separate 'master' database and the multi-tenant application will pick up the existing tenants and any tenant added while your application is running. If you were to add a new tenant in the application.yml file, then you would have to restart the application. With the technique I am about to describe, you do not need to restart your application.

Structure of the application

In the previous posts, the application learnt about the tenants from the application.yml file. In this post, the tenant information is not learnt from the application.yml file but from a separate table in the master database.






















The application.yml file is still required for application related properties and values, but the tenant database information need not be stored in it.

Datasources - One for the master database and one each for every tenant

The tenant information will be stord in the master database. For example, if the master database is called 'masterdb' and the table for tenant information is called 'master_tenant', then the information might look like this:










It is a simple table with the datasource related information about each tenant, like url, username, password, etc.


Order of setting up datasources and entity managers

It is important how the data sources are set up. Earlier, the tenant information was from the application.yml file. So the only datasources being setup were for each tenant.

The datasource for the 'master_tenant' table needs to be setup first as this table contains the tenant information. Once this datasource is set up, it can be used to read the tenant information and set up a datasource per tenant.

Please pay attention to the package structures. The master and tenant related datasource configurations are kept in separate packages and the master datasource is set up first.





































The following source code listings show the way this is done.

File: MasterDatabaseConfig.java

This sets up the master datasource to connect to the masterdb database.

package com.sunitkatkar.blogspot.master.config;

import java.util.Properties;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.dao.annotation.
        PersistenceExceptionTranslationPostProcessor;
import org.springframework.data.jpa.repository.config.
        EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.
        LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.
        HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.
        EnableTransactionManagement;

import com.sunitkatkar.blogspot.master.model.MasterTenant;
import com.sunitkatkar.blogspot.master.repository.
        MasterTenantRepository;
import com.zaxxer.hikari.HikariDataSource;

/**
 * Configuration of the master database which holds information about tenants in
 * the application.
 * 
 * @author Sunit Katkar, sunitkatkar@gmail.com
 *         (https://sunitkatkar.blogspot.com/)
 * @since ver 1.0 (May 2018)
 * @version 1.0
 */
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "com.sunitkatkar.blogspot.master.model",
                                 "com.sunitkatkar.blogspot.master.repository" }, 
                       entityManagerFactoryRef = "masterEntityManagerFactory", 
                       transactionManagerRef = "masterTransactionManager")
public class MasterDatabaseConfig {

    private static final Logger LOG = LoggerFactory
            .getLogger(MasterDatabaseConfig.class);

    /**
     * Master database configuration properties like username, password, etc.
     */
    @Autowired
    private MasterDatabaseConfigProperties masterDbProperties;

    /**
     * Creates the master datasource bean which is required for creating the
     * entity manager factory bean <br/>
     * <br/>
     * Note that using names for beans is not mandatory but it is a good
     * practice to ensure that the intended beans are being used where required.
     * 
     * @return
     */
    @Bean(name = "masterDataSource")
    public DataSource masterDataSource() {

        LOG.info("Setting up masterDataSource with: "
                + masterDbProperties.toString());

        HikariDataSource ds = new HikariDataSource();

        ds.setUsername(masterDbProperties.getUsername());
        ds.setPassword(masterDbProperties.getPassword());
        ds.setJdbcUrl(masterDbProperties.getUrl());
        ds.setDriverClassName(masterDbProperties.getDriverClassName());
        ds.setPoolName(masterDbProperties.getPoolName());

        // HikariCP settings
        // Maximum number of actual connection in the pool
        ds.setMaximumPoolSize(masterDbProperties.getMaxPoolSize());

        // Minimum number of idle connections in the pool
        ds.setMinimumIdle(masterDbProperties.getMinIdle());

        // Maximum waiting time for a connection from the pool
        ds.setConnectionTimeout(masterDbProperties.getConnectionTimeout());

        // Maximum time that a connection is allowed to sit idle in the pool
        ds.setIdleTimeout(masterDbProperties.getIdleTimeout());
        LOG.info("Setup of masterDataSource succeeded.");
        return ds;
    }

    /**
     * Creates the entity manager factory bean which is required to access the
     * JPA functionalities provided by the JPA persistence provider, i.e.
     * Hibernate in this case. <br/>
     * <br/>
     * Note the <b>{@literal @}Primary</b> annotation which tells Spring boot to
     * create this entity manager as the first thing when starting the
     * application.
     * 
     * @return
     */
    @Primary
    @Bean(name = "masterEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean masterEntityManagerFactory(){
        LocalContainerEntityManagerFactoryBean em = 
                new LocalContainerEntityManagerFactoryBean();

        // Set the master data source
        em.setDataSource(masterDataSource());

        // The master tenant entity and repository need to be scanned
        em.setPackagesToScan(
                new String[]{MasterTenant.class.getPackage().getName(),
                        MasterTenantRepository.class.getPackage().getName()});
        // Setting a name for the persistence unit as Spring sets it as
        // 'default' if not defined
        em.setPersistenceUnitName("masterdb-persistence-unit");

        // Setting Hibernate as the JPA provider
        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);

        // Set the hibernate properties
        em.setJpaProperties(hibernateProperties());
        LOG.info("Setup of masterEntityManagerFactory succeeded.");
        return em;
    }

    /**
     * This transaction manager is appropriate for applications that use a
     * single JPA EntityManagerFactory for transactional data access. <br/>
     * <br/>
     * Note the <b>{@literal @}Qualifier</b> annotation to ensure that the
     * <tt>masterEntityManagerFactory</tt> is used for setting up the
     * transaction manager.
     * 
     * @param emf
     * @return
     */
    @Bean(name = "masterTransactionManager")
    public JpaTransactionManager masterTransactionManager(
            @Qualifier("masterEntityManagerFactory") EntityManagerFactory emf) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(emf);
        return transactionManager;
    }

    /**
     * Bean post-processor that automatically applies persistence exception
     * translation to any bean marked with Spring's @Repository annotation,
     * adding a corresponding PersistenceExceptionTranslationAdvisor to the
     * exposed proxy (either an existing AOP proxy or a newly generated proxy
     * that implements all of the target's interfaces).
     * 
     * @return
     */
    @Bean
    public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
        return new PersistenceExceptionTranslationPostProcessor();
    }

    /**
     * The properties for configuring the JPA provider Hibernate.
     * 
     * @return
     */
    private Properties hibernateProperties() {
        Properties properties = new Properties();
        properties.put(org.hibernate.cfg.Environment.DIALECT,
                "org.hibernate.dialect.MySQL5Dialect");
        properties.put(org.hibernate.cfg.Environment.SHOW_SQL, true);
        properties.put(org.hibernate.cfg.Environment.FORMAT_SQL, true);
        properties.put(org.hibernate.cfg.Environment.HBM2DDL_AUTO, "update");
        return properties;
    }
}


File: TenantDatabaseConfig.java

This class sets up the datasources for the tenant databases. Note that this class requires the 'MultiTenantConnectionProvider' for the tenant datasources. These datasources are configured in the 'DataSourceBasedMultiTenantConnectionProviderImpl' class, shown later on in this post.

package com.sunitkatkar.blogspot.tenant.config;

import java.util.HashMap;
import java.util.Map;

import javax.persistence.EntityManagerFactory;

import org.hibernate.MultiTenancyStrategy;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.sunitkatkar.blogspot.tenant.model.User;
import com.sunitkatkar.blogspot.tenant.repository.UserRepository;
import com.sunitkatkar.blogspot.tenant.service.UserService;

/**
 * This is the tenant data sources configuration which sets up the multitenancy.
 * 
 * @author Sunit Katkar, sunitkatkar@gmail.com
 *         (https://sunitkatkar.blogspot.com/)
 * @since ver 1.0 (May 2018)
 * @version 1.0
 */
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = { "com.sunitkatkar.blogspot.tenant.repository",
        "com.sunitkatkar.blogspot.tenant.model" })
@EnableJpaRepositories(basePackages = {
        "com.sunitkatkar.blogspot.tenant.repository",
        "com.sunitkatkar.blogspot.tenant.service" }, 
        entityManagerFactoryRef = "tenantEntityManagerFactory", 
        transactionManagerRef = "tenantTransactionManager")
public class TenantDatabaseConfig {

    private static final Logger LOG = LoggerFactory
            .getLogger(TenantDatabaseConfig.class);


    @Bean(name = "tenantJpaVendorAdapter")
    public JpaVendorAdapter jpaVendorAdapter() {
        return new HibernateJpaVendorAdapter();
    }

    @Bean(name = "tenantTransactionManager")
    public JpaTransactionManager transactionManager(
            EntityManagerFactory tenantEntityManager) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(tenantEntityManager);
        return transactionManager;
    }

    /**
     * The multi tenant connection provider
     * 
     * @return
     */
    @Bean(name = "datasourceBasedMultitenantConnectionProvider")
    @ConditionalOnBean(name = "masterEntityManagerFactory")
    public MultiTenantConnectionProvider multiTenantConnectionProvider() {
        // Autowires the multi connection provider
        return new DataSourceBasedMultiTenantConnectionProviderImpl();
    }

    /**
     * The current tenant identifier resolver
     * 
     * @return
     */
    @Bean(name = "currentTenantIdentifierResolver")
    public CurrentTenantIdentifierResolver currentTenantIdentifierResolver() {
        return new CurrentTenantIdentifierResolverImpl();
    }

    /**
     * Creates the entity manager factory bean which is required to access the
     * JPA functionalities provided by the JPA persistence provider, i.e.
     * Hibernate in this case.
     * 
     * @param connectionProvider
     * @param tenantResolver
     * @return
     */
    @Bean(name = "tenantEntityManagerFactory")
    @ConditionalOnBean(name = "datasourceBasedMultitenantConnectionProvider")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            @Qualifier("datasourceBasedMultitenantConnectionProvider") 
            MultiTenantConnectionProvider connectionProvider,
            @Qualifier("currentTenantIdentifierResolver") 
            CurrentTenantIdentifierResolver tenantResolver) {

        LocalContainerEntityManagerFactoryBean emfBean = new LocalContainerEntityManagerFactoryBean();
        //All tenant related entities, repositories and service classes must be scanned
        emfBean.setPackagesToScan(
                new String[] { User.class.getPackage().getName(),
                        UserRepository.class.getPackage().getName(),
                        UserService.class.getPackage().getName() });
        emfBean.setJpaVendorAdapter(jpaVendorAdapter());
        emfBean.setPersistenceUnitName("tenantdb-persistence-unit");
        Map<String, Object> properties = new HashMap<>();
        properties.put(org.hibernate.cfg.Environment.MULTI_TENANT,
                MultiTenancyStrategy.SCHEMA);
        properties.put(
                org.hibernate.cfg.Environment.MULTI_TENANT_CONNECTION_PROVIDER,
                connectionProvider);
        properties.put(
                org.hibernate.cfg.Environment.MULTI_TENANT_IDENTIFIER_RESOLVER,
                tenantResolver);
        // ImprovedNamingStrategy is deprecated and unsupported in Hibernate 5
        // properties.put("hibernate.ejb.naming_strategy",
        // "org.hibernate.cfg.ImprovedNamingStrategy");
        properties.put(org.hibernate.cfg.Environment.DIALECT,
                "org.hibernate.dialect.MySQL5Dialect");
        properties.put(org.hibernate.cfg.Environment.SHOW_SQL, true);
        properties.put(org.hibernate.cfg.Environment.FORMAT_SQL, true);
        properties.put(org.hibernate.cfg.Environment.HBM2DDL_AUTO, "update");

        emfBean.setJpaPropertyMap(properties);
        LOG.info("tenantEntityManagerFactory set up successfully!");
        return emfBean;
    }
}


File: DataSourceBasedMultiTenantConnectionProviderImpl.java

This sets up the multi-tenant connection properties required by Hibernate. This is different than the code you have seen in the past 2 blog posts. Here, the datasources are read from the 'master_tenant' table and then stored in a map. This map is used to look up the datasource based on the tenant id. If a tenant is added to the 'master_tenant' table while the application is running, then a request for that tenant will prompt this class to read the 'master_tenant' table once again and recreate the datasource look up map. This is how the application becomes 'dynamic' about using a tenant added at runtime.

package com.sunitkatkar.blogspot.tenant.config;

import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import javax.sql.DataSource;

import org.hibernate.engine.jdbc.connections.
    spi.AbstractDataSourceBasedMultiTenantConnectionProviderImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;

import com.sunitkatkar.blogspot.master.model.MasterTenant;
import com.sunitkatkar.blogspot.master.repository.MasterTenantRepository;
import com.sunitkatkar.blogspot.util.DataSourceUtil;

/**
 * This class does the job of selecting the correct database based on the tenant
 * id found by the {@link CurrentTenantIdentifierResolverImpl}
 * 
 * @author Sunit Katkar, sunitkatkar@gmail.com
 *         (https://sunitkatkar.blogspot.com/)
 * @since ver 1.0 (May 2018)
 * @version 1.0
 *
 */
@Configuration
public class DataSourceBasedMultiTenantConnectionProviderImpl
        extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl {

    private static final Logger LOG = LoggerFactory
            .getLogger(DataSourceBasedMultiTenantConnectionProviderImpl.class);

    private static final long serialVersionUID = 1L;

    /**
     * Injected MasterTenantRepository to access the tenant information from the
     * master_tenant table
     */
    @Autowired
    private MasterTenantRepository masterTenantRepo;

    /**
     * Map to store the tenant ids as key and the data source as the value
     */
    private Map<String, DataSource> dataSourcesMtApp = new TreeMap<>();

    @Override
    protected DataSource selectAnyDataSource() {
        // This method is called more than once. So check if the data source map
        // is empty. If it is then rescan master_tenant table for all tenant
        // entries.
        if (dataSourcesMtApp.isEmpty()) {
            List<MasterTenant> masterTenants = masterTenantRepo.findAll();
            LOG.info(">>>> selectAnyDataSource() -- Total tenants:"
                    + masterTenants.size());
            for (MasterTenant masterTenant : masterTenants) {
                dataSourcesMtApp.put(masterTenant.getTenantId(), DataSourceUtil
                        .createAndConfigureDataSource(masterTenant));
            }
        }
        return this.dataSourcesMtApp.values().iterator().next();
    }


    @Override
    protected DataSource selectDataSource(String tenantIdentifier) {
        // If the requested tenant id is not present check for it in the master
        // database 'master_tenant' table
        if (!this.dataSourcesMtApp.containsKey(tenantIdentifier)) {
            List<MasterTenant> masterTenants = masterTenantRepo.findAll();
            LOG.info(">>>> selectDataSource() -- tenant:" + tenantIdentifier
                    + " Total tenants:" + masterTenants.size());
            for (MasterTenant masterTenant : masterTenants) {
                dataSourcesMtApp.put(masterTenant.getTenantId(), DataSourceUtil
                        .createAndConfigureDataSource(masterTenant));
            }
        }
        return this.dataSourcesMtApp.get(tenantIdentifier);
    }
}


Resources

The complete source code is checked into GitHub. Its a standard Maven project which you can import into your IDE.

In conclusion

That's all there is to reading tenant information at run time. For a production grade application, I recommend that you create a separate Spring Boot app or a microservice to manage the tenant information in the master_tenant table. So this keeps the management of the application separate from the actual application. The master database can be used to store other kinds of app related information and the separate app can be used by the administrators for other app related activities.

Happy coding :)

Update - 10 July 2018

A reader of my blog - Jordan Mackie - found an error which was a mistake on my part in publishing a small but important part of the code. In the file TenantDatabaseConfig.java you need to explicitly name the tenant entity manager factory and use that when passing it in to the transaction manager. So the code should look like
@Bean(name = "tenantTransactionManager")
    public JpaTransactionManager transactionManager(
@Qualifier("tenantEntityManagerFactory")
EntityManagerFactory tenantEntityManager) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(tenantEntityManager);
        return transactionManager;
    }

Thursday, May 10, 2018

Spring Boot 2 generic JPA converter to serialize and deserialize an entity attribute as JSON

As I wrote in my previous blog post, I needed to store some custom attributes of entities as JSON strings in the database.

Sometimes an entity attribute is not just a native or simple type like a Date, Long or String. A Java POJO like structure is required as an attribute. One might argue that a separate entity should be created which results in a separate table and a reference to it should be used like in a OneToOne or ManyToOne or OneToMany type of relationship. But there are special business conditions where creating a separate entity is not feasible. So, storing this entity as a JSON string is a good solution.

Purpose of this blog post

In the last blog post, I shared how I wrote a generic encryption decryption JPA Converter. In this blog post I will show you how to store a custom Java POJO like attribute as a JSON string in a database table column.

JPA allows writing 'conversion' code so that an entity attribute (table column) can be converted from one type to another and back. Though this is not a latest feature of JPA nor a bleeding edge topic, I am sharing how I wrote a generic converter for serialization and serialization of a Java POJO object to a string type table column.

Java and JSON

I am assuming you know what JSON is and how it is used for data transformation. If you need to know more then click here.

In our case, we need to convert some Java POJO like JPA attribute to its JSON equivalent and store it in the database table column. Similarly, we need to parse the JSON format and convert it back to the original Java POJO like attribute.

Java and Jackson

Given that JSON is not a complex format, one can attempt to convert a Java POJO like object into a JSON String and back by writing code for each such object. However, this is a tedious and error-prone endeavor. And why do it all yourself when there is a reliable, widely used library to do this? Faster Jackson is an awesome library to help convert your Java POJO like object into JSON (and XML too) and parse the JSON back into the original Java POJO object.

If you are new to Jackson or want to read more, then click here. There is a good tutorial here. For a more Spring framework specific tutorial, click here.

JPA and Converter

JPA allows you to apply a @Converter annotation to any Entity attribute i.e. to any table column definition in your table defintion. This is possible via the AttributeConveter interface. A class implementing this interface needs to implement the methods from this interface


  • Y convertToDatabaseColumn(X attribute) - Converts the value stored in the entity attribute into the data representation to be stored in the database.
  • X convertToEntityAttribute(Y dbData) -  Converts the data stored in the database column into the value to be stored in the entity attribute. Note that it is the responsibility of the converter writer to specify the correct dbData type for the corresponding column for use by the JDBC driver: i.e., persistence providers are not expected to do such type conversion.

Writing the generic converter

Please keep these two Jackson JSON javadocs open in your browser so that you can refer to them while reading the code I am presenting.
Though we do not need it for this converter, you can also look at the Jackson JSON Annotation javadocs for more details.

Note that for a good error free conversion of your Java POJO like object, one of the requirements is to provide the class type information to the Jackson ObjectMapper. It is not an absolute must, but having it is better. Also for a generic converter, it becomes quite essential to tell the converter about the Class type of the object.

The generic JSON JPA converter code

Here is the converter code. We just need one concrete class which can then be extended to convert a specific Java POJO like attribute.


package com.sunitkatkar.blogspot.converters;

import java.io.IOException;

import javax.persistence.AttributeConverter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;

/**
 * JPA converter class which converts any POJO like object to a JSON string and
 * stores it in the database table column. The JSON string contains the class
 * name of the POJO and its JSON format string. <br/>
 * 
 * Note: An abstract class is not needed as this class takes care of all the
 * JSON serialization and deserialization. You only need to extend this class so
 * that you can use your POJO class for conversion. The extended class should be
 * annotated with the {@literal @}Converter annotation
 * 
 * @author Sunit Katkar, sunitkatkar@gmail.com
 *         (https://sunitkatkar.blogspot.com/)
 * @since ver 1.0 (May 2018)
 * @version 1.0
 * @param <X>
 */
public class GenericJsonAttributeConverter<X>
        implements AttributeConverter<X, String> {

    protected static final Logger LOG = LoggerFactory
            .getLogger(GenericJsonAttributeConverter.class);

    /**
     * To 'write' the attribute POJO as a JSON string
     */
    private final ObjectWriter writer;

    /**
     * To 'read' the JSON string and convert to the attribute POJO
     */
    private final ObjectReader reader;

    /**
     * Default constructor
     */
    public GenericJsonAttributeConverter() {

        ObjectMapper mapper = new ObjectMapper();

        // We want all keys in the key value pair JSON even if the key has no
        // value
        mapper.setSerializationInclusion(Include.ALWAYS);

        //@formatter:off
        // The mapper should be able to 'see' the entity attributes (fields)
        mapper.setVisibility(mapper.getSerializationConfig()
                .getDefaultVisibilityChecker()
                .withFieldVisibility(JsonAutoDetect.Visibility.ANY)
                .withGetterVisibility(JsonAutoDetect.Visibility.NONE)
                .withIsGetterVisibility(JsonAutoDetect.Visibility.NONE));
        //@@formatter:on

        // We are wrapping the entity attribute in a class. See class for
        // details.
        reader = mapper.reader().forType(JsonTypeLike.class);
        writer = mapper.writer().forType(JsonTypeLike.class);
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * javax.persistence.AttributeConverter#convertToDatabaseColumn(java.lang.
     * Object)
     */
    @Override
    public String convertToDatabaseColumn(X attribute) {
        try {
            if (attribute != null) {
                JsonTypeLike<X> wrapper = new JsonTypeLike<X>(attribute,
                        writer);
                String value = writer.writeValueAsString(wrapper);
                return value;
            } else {
                return null;
            }
        } catch (JsonProcessingException e) {
            LOG.error("Failed to serialize as object into JSON: {}", attribute,
                    e);
            throw new RuntimeException(e);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * javax.persistence.AttributeConverter#convertToEntityAttribute(java.lang.
     * Object)
     */
    @Override
    public X convertToEntityAttribute(String dbData) {
        try {
            if (dbData != null) {
                JsonTypeLike<X> wrapper = reader.readValue(dbData);
                X obj = wrapper.readValue(reader);
                return obj;
            } else {
                return null;
            }
        } catch (IOException e) {
            LOG.error("Failed to deserialize as object from JSON: {}", dbData,
                    e);
            throw new RuntimeException(e);
        }
    }

    /**
     * The concrete type is needed for Jackson to serialize or deserialize. This
     * class is created to wrap the entity type &lt;Y&gt; so that the Jackson
     * {@link ObjectReader#forType(Class)} and
     * {@link ObjectWriter#forType(Class))} can be used to get the concrete type
     * of the attribute being serialized/deserialized.
     * 
     * @author Sunit Katkar, sunitkatkar@gmail.com
     *
     * @param <Y>
     */
    public static class JsonTypeLike<Y> {

        // For adding the type (class) of the entity in the generated JSON
        private String entityType;

        // For the actual value which is obtained by reading or writing the JSON
        private String entityValue;

        public JsonTypeLike() {
        }

        /**
         * Constructor which helps initialize the ObjectWriter by providing the
         * concrete class type to the writer
         * 
         * @param obj
         * @param writer
         */
        public JsonTypeLike(Y obj, ObjectWriter writer) {
            // TODO: Need a better way to do type conversion and not get safety
            // warning
            Class<Y> classType = ((Class<Y>) obj.getClass());
            // We are saving the class type as a string in entityType so that
            // while reading
            // back the JSON, the target Class type is known o the mapper.
            this.entityType = obj.getClass().getName();

            try {
                this.entityValue = writer.forType(classType)
                        .writeValueAsString(obj);
            } catch (JsonProcessingException e) {
                LOG.error("Failed serializing object to JSON: {}", obj, e);
            }
        }

        /**
         * Read the JSON format string and create the target Java POJO object
         * 
         * @param reader
         * @return
         */
        public Y readValue(ObjectReader reader) {
            try {
                // Once the json string is read, we need to know what is the
                // destination class
                // type. So reading the entityType value helps in getting the
                // Class details.
                Class<?> clazz = Class.forName(this.entityType);
                Y obj = reader.forType(clazz).readValue(this.entityValue);
                return obj;
            } catch (ClassNotFoundException | IOException e) {
                LOG.error("Failed deserializing object from JSON: {}",
                        this.entityValue, e);
                return null;
            }
        }

        public String getEntityType() {
            return entityType;
        }

        public void setEntityType(String type) {
            this.entityType = type;
        }

        public String getEntityValue() {
            return entityValue;
        }

        public void setValue(String value) {
            this.entityValue = value;
        }
    }
}

Example for using the converter

Let us suppose there is a class called Student and it contains an attribute of another custom type called Course. The Course is a simple Java POJO which contains details about the course that a student enrolls in.

JPA Entity - Student

This JPA entity has a Course attribute. The Student entity will create a table called 'student' in the database and have a column/field called 'course'. This course attribute is the one which needs to be stored as a JSON string.

package com.sunitkatkar.blogspot.model;

import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.sunitkatkar.blogspot.converters.CourseJsonConverter;

/**
 * JPA Entity which will be saved in the database table named 'student'. The
 * attribute (or table column) <tt>course</tt> needs to be converted to a JSON
 * string for storage and converted back to {@link Course} POJO when read.
 * 
 * @author Sunit Katkar, sunitkatkar@gmail.com
 *         (https://sunitkatkar.blogspot.com/)
 * @since ver 1.0 (May 2018)
 * @version 1.0
 */
@Entity
@Table(name = "student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "student_id")
    private Long id;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "last_name")
    private String lastName;

    /**
     * This attribute is the Course POJO which needs to be stored as a JSON
     * string
     */
    @Convert(converter = CourseJsonConverter.class)
    @Column(name = "course")
    private Course course;

    /**
     * Just a field to get the Course POJO as a JSON string and display on the
     * UI
     */
    private String json;

    /**
     * @return the json
     */
    /**
     * Default constructor
     */
    public Student() {
    }

    // Getters and setters
    /**
     * @return the id
     */
    public Long getId() {
        return id;
    }

    /**
     * @param id
     *            the id to set
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * @return the firstName
     */
    public String getFirstName() {
        return firstName;
    }

    /**
     * @param firstName
     *            the firstName to set
     */
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    /**
     * @return the lastName
     */
    public String getLastName() {
        return lastName;
    }

    /**
     * @param lastName
     *            the lastName to set
     */
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    /**
     * @return the course
     */
    public Course getCourse() {
        return course;
    }

    /**
     * @param course
     *            the course to set
     */
    public void setCourse(Course course) {
        this.course = course;
    }

    /**
     * Method to get the Course POJO as a JSON string for display on UI
     * 
     * @return
     */
    public String getJson() {
        ObjectMapper mapper = new ObjectMapper();
        ObjectWriter writer = mapper.writer().forType(Course.class);

        String jsonString = "";
        try {
            jsonString = writer.withDefaultPrettyPrinter()
                    .writeValueAsString(this.course);
            // System.out.println(jsonString);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return jsonString;
    }

    /**
     * @param json
     *            the json to set
     */
    public void setJson(String json) {
        this.json = json;
    }
}

Course Java POJO

This is the Course attribute in the Student JPA entity. Do note that when you use java.util.Date fields you must annotate them with a @DateTimeFormat annotation so that Spring understands how to convert a string in a date like format to an actual date format. This is especially applicable when you have a browser based UI to enter dates.

package com.sunitkatkar.blogspot.model;

import java.util.Date;

import org.springframework.format.annotation.DateTimeFormat;

/**
 * This POJO is a attribute of the {@link Student} entity. This will be
 * serialized as a JSON string and stored in the 'course' field in the 'student'
 * table.
 * 
 * @author Sunit Katkar, sunitkatkar@gmail.com
 *         (https://sunitkatkar.blogspot.com/)
 * @since ver 1.0 (May 2018)
 * @version 1.0
 */
public class Course {

    private Long id;

    private String title;

    private String description;

    /**
     * The @DateTimeFormat annotation is required as the browser sends a date in
     * string format and Spring needs to translate it into a date. So we have to
     * specify a format for the date. Note that the browser always sends date in
     * the 'yyyy-MM-dd' format when using the HTML5 attribute type='date' on an
     * 'input' tag.
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date startDate;

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date endDate;

    /**
     * 
     */
    public Course() {
        super();
    }

    /**
     * @return the id
     */
    public Long getId() {
        return id;
    }

    /**
     * @param id
     *            the id to set
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * @return the title
     */
    public String getTitle() {
        return title;
    }

    /**
     * @param title
     *            the title to set
     */
    public void setTitle(String title) {
        this.title = title;
    }

    /**
     * @return the description
     */
    public String getDescription() {
        return description;
    }

    /**
     * @param description
     *            the description to set
     */
    public void setDescription(String description) {
        this.description = description;
    }

    /**
     * @return the startDate
     */
    public Date getStartDate() {
        return startDate;
    }

    /**
     * @param startDate
     *            the startDate to set
     */
    public void setStartDate(Date startDate) {
        this.startDate = startDate;
    }

    /**
     * @return the endDate
     */
    public Date getEndDate() {
        return endDate;
    }

    /**
     * @param endDate
     *            the endDate to set
     */
    public void setEndDate(Date endDate) {
        this.endDate = endDate;
    }

}

Using the JSON JPA Converter

To use the converter, all you need to do is create a class which extends from it. The following example is applicable for the Student entity and its attribute Course. That's all there is to it.

import javax.persistence.Converter;

import com.sunitkatkar.blogspot.model.Course;

/**
 * Concrete converter class which extends the {@link GenericJsonAttributeConverter}
 * class. <br/>
 * Note: No need to do anything here. This is used to pass the attribute type to
 * the GenericJsonAttributeConverter
 * 
 * @author Sunit Katkar, sunitkatkar@gmail.com
 *         (https://sunitkatkar.blogspot.com/)
 * @since ver 1.0 (May 2018)
 * @version 1.0
 */
@Converter(autoApply = false)
public class CourseJsonConverter extends GenericJsonAttributeConverter<Course> {

}

Resources

I have included a complete working Spring Boot application to demonstrate how this JSON converter can be used. Visit my Github repository to download the code. You are free to fork it, but just request you to send me an email when you do so.

Happy coding :)