Database - Devops - Docker-compose - Kubernetes - Mysql

Container Managed Database Resource (JNDI) with Springboot.

We have the example of applications which are connecting to database are managed by the application. Sometime applications wants it to managed by the container. Application maintained the database resource properties in the application yaml properties and when the application started spring boot takes care of building the resources using the starters. With this the application start manage the resources as well.

Applications manage the responsibility of the operations and resource management is left on the container. This is the complete segregation of usage and management of the resource as the application is using the resource by read, update etc operation and container is managing the resource by providing the connection resources. we will check one of our application where I will describe how to enable container managed resources.

Codebase

We are making these changes in our billing application. The source code is present at the following link

You can find the installation step at the root of the project.

Approach

If you remember, we defined the resource inside the context.xml in the tomcat’s conf directory. We use the JNDI, to access the resource in the application code. Since with Springboot we are using embedded tomcat, then how this configuration is registered with the underlying tomcat. In next few section we will explore how to do achieve this.

APPLICATION

Lets go through the items that are affected to get this type of functionality in our codebase.

Properties File

We need tp create a separate properties file which is used to have the default pros for this configuration. Lets name this property file as database-application2.properties

This file will have the following contents

database.jndiName=jdbc/billing   
database.url= ${SPRING_DATASOURCE_URL:jdbc:mysql://${SERVICE:localhost}:${PORT:3306}/${DATABASE:billdb}}
database.auth= 
database.type= org.apache.tomcat.jdbc.pool.DataSource
database.username= ${SPRING_DATASOURCE_USERNAME:cpandey}
database.password= ${SPRING_DATASOURCE_PASSWORD:chandan@1234}
database.defaultAutoCommit= true
database.defaultReadOnly= false
database.driverClassName= ${SPRING_DATASOURCE_DRIVER_CLASS_NAME:com.mysql.jdbc.Driver}
database.maxActive= 10
database.maxIdle= 5
database.minIdle= 3
database.initialSize= 10
database.maxWait= 1000
database.testOnBorrow= true
database.testOnConnect= true 
database.testOnReturn= false
database.testWhileIdle= false
database.validationQuery= SELECT 1 FROM DUAL
database.validationQueryTimeout= 1000
database.validatorClassName= 
database.logAbandoned= true
database.removeAbandoned= true
database.removeAbandonedTimeOut= 30000
database.timeBetweenEvictionRunsMillis= 30000
database.minEvictableIdleTimeMillis= 30000
database.validationInterval= 30000
database.jdbcInterceptors= 
     - "org.apache.tomcat.jdbc.pool.interceptor.ConnectionState"
     - "org.apache.tomcat.jdbc.pool.interceptor.StatementFinalizer" 

##JdbcInterceptors("org.apache.tomcat.jdbc.pool.interceptor.ConnectionState;"+ "org.apache.tomcat.jdbc.pool.interceptor.StatementFinalizer");

Configuration

We will be creating the following configuration files to digest these above properties in the application.

DataSourceConfigJndi

This configuration file will have the following content. There are some attributes are omitted as this file is bit long. The essence of this file is that, it read the attribute from the environment variables, if it does not present there then it will be defaulted to properties file.

package com.itlogiclab.billing.config;

import java.util.Objects;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;

import com.itlogiclab.billing.utils.BillingConstants;

@Configuration
@PropertySource("classpath:database-application2.properties")
@ConfigurationProperties(prefix = "database")
public class DataSourceConfigJndi{
	
	@Autowired
	private Environment env;

	private String jndiName;

	private String auth;

	private String type; 
	
...... More attribute 

	/**
	 * @return the name
	 */
	public String getJndiName() {
		return Objects.isNull(env.getProperty(BillingConstants.ENV_JNDI_NAME)) ? 
		jndiName : 
		env.getProperty(BillingConstants.ENV_JNDI_NAME);
	}


	/**
	 * @return the auth
	 */
	public String getAuth() {
		return Objects.isNull(env.getProperty(BillingConstants.ENV_AUTH)) ? 
		auth : 
		env.getProperty(BillingConstants.ENV_AUTH);
	}

.......... More getters
	
	

	/**
	 * @param env the env to set
	 */
	public void setEnv(Environment env) {
		this.env = env;
	}


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


	/**
	 * @param auth the auth to set
	 */
	public void setAuth(String auth) {
		this.auth = auth;
	}
............ More Setters

TomcatFactoryConfig

This factory id used to register the JNDI name, which used to register the context with the embedded tomcat.

 package com.itlogiclab.billing.config;

## Removing imports 

@Configuration
@EnableJpaRepositories(basePackages = "com.itlogiclab.billing.repos")
@EnableTransactionManagement
public class TomcatFactoryConfig {
	private static Logger logger = LogManager.getLogger(TomcatFactoryConfig.class); 

	@Bean
	public DataSourceConfigJndi getDataSourceConfig() {
		return new DataSourceConfigJndi();
	}
	
	@Bean
	public TomcatServletWebServerFactory getTomcatFactory() {
		TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
			@Override
			public TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
				tomcat.enableNaming();
				return super.getTomcatWebServer(tomcat);
			}
			
			@Override
	        protected void postProcessContext(Context context) {
				
		    DataSourceConfigJndi jndiProps = getDataSourceConfig();
				logger.debug("DataSource Props: "+jndiProps);
				
				ContextResource resource = new ContextResource();
	            resource.setName(getDataSourceConfig().getJndiName());
	            resource.setType(DataSource.class.getName());
	            
	            resource.setProperty(BillingConstants.FACTORY, "org.apache.tomcat.jdbc.pool.DataSourceFactory");
	            
	            resource.setProperty(BillingConstants.DRIVER_CLASS_NAME, jndiProps.getDriverClassName());
	           

	            resource.setProperty(BillingConstants.URL, jndiProps.getUrl());
	            resource.setProperty(BillingConstants.USERNAME, jndiProps.getUsername());
	            resource.setProperty(BillingConstants.PASSWORD, jndiProps.getPassword());
	            esource.setProperty(BillingConstants.MIN_IDLE, jndiProps.getMinIdle());
	            resource.setProperty(BillingConstants.MAX_ACTIVE, jndiProps.getMaxActive());
	            resource.setProperty(BillingConstants.MAX_IDLE, jndiProps.getMaxIdle());
	            resource.setProperty(BillingConstants.MAX_WAIT, jndiProps.getMaxWait());
	            
	            ## Removing few other properties 
	            
	            context.getNamingResources().addResource(resource);  
	            ## Naming resource is created and register with tomcat
	        }
	    };
	    return tomcat;
	}
	
	@Bean(destroyMethod = "")
	public DataSource jndiDataSource() throws IllegalArgumentException, NamingException {
		JndiObjectFactoryBean bean = new JndiObjectFactoryBean();
		bean.setJndiName(BillingConstants.JAVA_COMP_ENV + getDataSourceConfig().getJndiName());
		bean.setProxyInterface(DataSource.class);
		bean.setLookupOnStartup(false);
		bean.afterPropertiesSet();
		return (DataSource) bean.getObject();
	}
	
	@Bean
	public EntityManagerFactory entityManagerFactory() throws SQLException, IllegalArgumentException, NamingException {
		
		HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
		vendorAdapter.setDatabase(Database.MYSQL);
		vendorAdapter.setShowSql(true);
		LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
		factory.setJpaVendorAdapter(vendorAdapter);
		factory.setPackagesToScan(new String[]{BillEntity.class.getPackage().getName()});
## Entities are are scanned and register with the hibernateJPAVendorAdaptor 
		factory.setDataSource(jndiDataSource());
		
		factory.afterPropertiesSet();
		return factory.getObject();
	}
	
	@Bean
	public PlatformTransactionManager transactionManager()throws SQLException, IllegalArgumentException, NamingException {
		JpaTransactionManager txManager = new JpaTransactionManager();
		txManager.setEntityManagerFactory(entityManagerFactory());
		return txManager;
	}
	
	
}

Deployment

Lets try out the deployment using docker-compose and kubernetes and see the results.

WITH docker-compose

Steps for the deployment using docker-compose is listed down in this blog. In this page we will be documenting the delta needed to run this.

.env file

This file is used to hold the configurable attributes, which are getting used in the docker-compose file. Here is a snapshot for this file.

##GLOBAL VARIABLES: 
ROOT_PASSWORD=root ##Root password for MySql
DB_USER=cpandey    ## DB username for mySql
DB_PASSWORD=       ##DB Password
SHOW_SQL=true
DDL_OPTION=none

BASE_LOC= ##Base Path from the root for the mount files. 
##e.g. /opt/itlogiclab/

##BILLING VARIABLE
MYSQLDB_BILLING_DATABASE=billdb ##DB Name. 
MYSQLDB_BILLING_LOCAL_PORT=3311 ## System port for mySql
MYSQLDB_BILLING_DOCKER_PORT=3306 ## Container port where  mySQL is running.

##JDBC URL for the application
MYSQLDB_BILLING_URL=jdbc:mysql://billing_data_db:$MYSQLDB_BILLING_DOCKER_PORT/$MYSQLDB_BILLING_DATABASE?useSSL=false

##System port where web app is accessible 
BILLING_WEB_LOCAL_PORT=20001
##Container port where this app is running
BILLING_DOCKER_WEB_PORT=20002

#System location for the mySQL Data 
BILLING_MYSQL_DATA_LOC=docker-data/restaurant/billing/mysql/data
#System Location for the Initial script. This file should be present at this location
BILLING_INITIAL_SCRIPT_LOC=docker-data/restaurant/billing/mysql/script/bil
ling.sql
##Log location in the system.
BILLING_LOG_LOC=docker-data/restaurant/billing/logs
Note: BASE_LOC is prepend with BILLING_MYSQL_DATA_LOC and other LOC attribute to make complete path. 
docker-compose-billing.yaml

This file contain the information about the services and their interaction.

version: "3.8"

services:  ## MYSQL Service
  billing_data_db:
    container_name: billing-percona-db
    platform: linux/x86_64
    image: percona:ps-5.6
    restart: unless-stopped
    env_file: ./.env      ## ENV file to use
    environment:
      - MYSQL_ROOT_PASSWORD=$ROOT_PASSWORD
      - MYSQL_USER=$DB_USER
      - MYSQL_PASSWORD=$DB_PASSWORD
      - MYSQL_DATABASE=$MYSQLDB_BILLING_DATABASE
      - MYSQL_ROOT_HOST="%"
    ports:
      - $MYSQLDB_BILLING_LOCAL_PORT:$MYSQLDB_BILLING_DOCKER_PORT
    volumes:
      - type: bind 
        source: $BASE_LOC/$BILLING_MYSQL_DATA_LOC 
        target: /var/lib/mysql
        volume:
          nocopy: true
      - type: bind
        source: $BASE_LOC/$BILLING_INITIAL_SCRIPT_LOC
        target: /docker-entrypoint-initdb.d/1.sql
        volume:
          nocopy: true
  billing_web_app:
      depends_on:
        - billing_data_db
      container_name: billing-web-app
      build:
        context: ./ 
        dockerfile: Dockerfile
      restart: on-failure
      env_file: ./.env
      ports:
        - $BILLING_WEB_LOCAL_PORT:$BILLING_DOCKER_WEB_PORT
      environment: ## Env variable used for DB connectivity. 
          SPRING_DATASOURCE_URL: "$MYSQLDB_BILLING_URL"
          SPRING_DATASOURCE_USERNAME : "$DB_USER"
          SPRING_DATASOURCE_PASSWORD : "$DB_PASSWORD"
          SPRING_DATASOURCE_DRIVER_CLASS_NAME: "com.mysql.cj.jdbc.Driver"
      volumes:
        - type: bind
          source: $BASE_LOC/$BILLING_LOG_LOC
          target: /opt/multimoduleprojects/restaurant/billing/logs
          volume:
            nocopy: true
      stdin_open: true
      tty: true

 

Lets try to deploy the application and see the outcome.

(base) cpandey@Chandans-MacBook-Pro itlogiclab-restaurant-billing % docker-compose -f docker-compose-billing.yaml up -d
[+] Running 2/2
 ⠿ Container billing-percona-db  Running                                                                                                                                                                                                                                 0.0s
 ⠿ Container billing-web-app     Running

Here the application is deployed on docker in local system. We can go ahead and test some api’s supported by the application.

Note:

To test the application we need to be on the root of the project. The root of the application is

ROOT: <PATH>/itlogiclab-restaurant-parent

Check to which port the application is exposed. use the docker ps command and check the PORTS section, which look like the below.

d4bf338a921c   .......   0.0.0.0:20001->20002/tcp
20001 is the PORT where the endpoints are exposed. 

Testing application

GET Operation

Let us checked the GET operation by using test scripts. Use the following command and pass the argument as port number. Also you can update the billing_add.json file if you want to pass some other values of the request.

./deployment/billing-deployment/test/get.sh 20001

This result the following output.

*********************************************************************
GET FIRST THE MESSAGE WITH URL http://localhost:20001/billing/api/get
[{"billId":13332,"customerId":1,"invoiceNo":"INV_1142","billAmount":2445.0,"date":"23//10//2022 14:25:45.222","orderId":2,"amount":2445,"status":null,"billInfoList":[{"billInfoId":13334,"invoiceNo":null,"chargeName":"IPHONE-12","chargeCost":148000.0},{"billInfoId":13336,"invoiceNo":null,"chargeName":"Apple Watch","chargeCost":42000.0},{"billInfoId":13333,"invoiceNo":null,"chargeName":"MAC","chargeCost":125000.0},{"billInfoId":13335,"invoiceNo":null,"chargeName":"AIRPOD-2nd GEN","chargeCost":25000.0}]}]%
Add Operation

Add operation is as simple as get to test. I have kept billing_add.json which is used for the request. Please run the following command

./deployment/billing-deployment/test/add.sh 20001

You should be on the root of the project. This command result in the following output.

{"billId":13352,"customerId":1,"invoiceNo":"INV_1142","billAmount":2445.0,"date":"23//10//2022 14:25:45.222","orderId":2,"amount":2445,"status":null,"billInfoList":[{"billInfoId":13353,"invoiceNo":null,"chargeName":"IPHONE-12","chargeCost":148000.0},{"billInfoId":13354,"invoiceNo":null,"chargeName":"AIRPOD-2nd GEN","chargeCost":25000.0},{"billInfoId":13355,"invoiceNo":null,"chargeName":"Apple Watch","chargeCost":42000.0},{"billInfoId":13356,"invoiceNo":null,"chargeName":"MAC","chargeCost":125000.0}]}

Similarly you can validate the update and delete scenarios. I trust you that you can test further from here.

Conclusion

Hence we learn, how to configure the JNDI with springboot application. This can be deployed on docker and we can test it. So with this approach we handover the responsibility of resource management to the tomcat.