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.
Prev Post
Next Post