I am in the process of migrating existing VMSS tomcat project to AKS. In VM, we use server.xml to provide environment variables using <Environment .../> tags. For Kubernetes, I am using env: field to replace these values.
In the project, there is code Config.java:
private Object getObject(String key) {
try {
log.debug("looking up key: " + key + " in environment context.");
Object rtn = getEnvContext().lookup(key);
log.debug("Found property for " + key + ": " + rtn);
return rtn;
} catch (NamingException e) {
log.debug("Unable to find key: " + key);
return null;
}
}
private Context getEnvContext() throws NamingException {
Context initCtx = new InitialContext();
Context envCtx = (Context) initCtx.lookup("java:comp/env");
return envCtx;
}
From what is java:comp/env?, it says that java:comp/env is the node in the JNDI tree where you can find properties for the current Java EE component.
Here are my questions.
When tomcat starts, does it use server.xml <Environment .../> tag to create properties in the node in the JNDI tree? (thus, when .getObject(someString) is called, it can get the value defined in server.xml)
If I want to replace server.xml with Helm chart env field, will these properties (defined in Helm) be defined in the node in the JNDI tree? or do I need to define these beans in jndi.xml as JndiObjectFactoryBean?
I am sorry if I am not making sense at all here. I am very new to tomcat and concept of JNDI and JNDI tree. Please let me know if I need to clarify myself.
Related
I have a requirement to read information that is available in the META/MANIFEST.MF file of Spring Boot MVC web application and use this info to perform some business logic. I'm using gradle to build the application as war file and deploying it into the external tomcat.
I have tried the following:
#Configuration
public class AppConfig
{
#Bean("manifest")
public java.util.jar.Manifest getManifest() throws IOException
{
InputStream inputFile = this.getClass().getClassLoader().getResourceAsStream("META-INF/MANIFEST.MF");
Manifest manifestObj = new Manifest(inputFile);
return manifestObj;
}
}
AppService.java
#Service
public class AppService
{
#Autowired
#Qualifier("manifest")
private Manifest manifest;
#PostConstruct
public String init()
{
Attributes mainAttributes = manifest.getMainAttributes();
String buildNum = mainAttributes.getValue("Build-Number");
String customPropInfo= mainAttributes.getValue("customPropInfo");
String systemPrp1= buildNum + "_" + "SomeBusinessInfoLogic1";
String systemPrp2= customPropInfo+ "_" + "SomeBusinessInfoLogic2";
//Some Business Logic with these attributes systemPrp, systemPrp2
logger.info("System Props are updated");
}
}
I'm getting null for both buildNum and customPropInfo.
Note: I have tried creating the Manifest bean something like this which was created by me. As per the #M.Deinum suggestion I'm creating this new question here. I also tried the solutions here which didn't work for me.
#M.Deinum suggested to make use of Spring Boot's Actuator Info endpoint. But this endpoint is useful when we want to access the info outside of the application but my requirement is different as I need the data that is available in MANIFEST.MF file to perform some business operations within the application.
I get the following error when I tried this solution "/META-INF/MANIFEST.MF".
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'manifest' defined in class path resource [com/abc/AppConfig.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [java.util.jar.Manifest]: Factory method 'getManifest' threw exception; nested exception is java.lang.NullPointerException: Cannot invoke "java.io.InputStream.read(byte[], int, int)" because "this.in" is null
Can someone please help me to read information from META/MANIFEST.MF of the Spring Boot MVC Web Application?.
UPDATE1: I get the following MainAttributes when I try to print MainAttributes. But the problem is when I try to deploy the war into external tomcat.
System.out.println("Manifest MainAttributes = " +manifestObj.getMainAttributes().keySet());
Output:
Manifest MainAttributes = [Manifest-Version, Implementation-Title, Automatic-Module-Name, Implementation-Version, Built-By, Spring-Boot-Jar-Type, Build-Jdk-Spec]
UPDATE2:
I have updated to AppService.java to print the info available in autowired Manifest object. Something like below:
#Configuration
public class AppConfig
{
#Bean("manifest")
public java.util.jar.Manifest getManifest() throws IOException
{
InputStream inputFile = new ClassPathResource("/META-INF/MANIFEST.MF").getInputStream();
Manifest manifestObj = new Manifest(inputFile);
System.out.println("Manifest Manifest-Version = " +manifestObj.getMainAttributes().getValue("Manifest-Version"));
System.out.println("Manifest KeySet = " +manifestObj.getMainAttributes().keySet());
return manifestObj;
}
}
#Service
public class AppService
{
#Autowired
#Qualifier("manifest")
private Manifest manifest;
#PostConstruct
public String init()
{
Attributes mainAttributes = manifest.getMainAttributes();
mainAttributes.forEach((k,v) -> {
System.out.println("AppService.init(): Key = "+k+", Value = "+v);
});
String buildNum = mainAttributes.getValue("Build-Number");
String customPropInfo= mainAttributes.getValue("customPropInfo");
String systemPrp1= buildNum + "_" + "SomeBusinessInfoLogic1";
String systemPrp2= customPropInfo+ "_" + "SomeBusinessInfoLogic2";
//Some Business Logic with these attributes systemPrp, systemPrp2
logger.info("System Props are updated");
}
}
I see the following output on the console:
AppService.init(): Key = Implementation-Title, Value = Apache Tomcat Bootstrap
AppService.init(): Key = Implementation-Version, Value = 9.0.12
AppService.init(): Key = Specification-Vendor, Value = Apache Software Foundation
AppService.init(): Key = Specification-Title, Value = Apache Tomcat Bootstrap
AppService.init(): Key = Class-Path, Value = commons-daemon.jar
AppService.init(): Key = Manifest-Version, Value = 1.0
AppService.init(): Key = Main-Class, Value = org.apache.catalina.startup.Bootstrap
AppService.init(): Key = Implementation-Vendor, Value = Apache Software Foundation
AppService.init(): Key = Ant-Version, Value = Apache Ant 1.9.9
AppService.init(): Key = X-Compile-Target-JDK, Value = 1.8
AppService.init(): Key = X-Compile-Source-JDK, Value = 1.8
AppService.init(): Key = Created-By, Value = some xyz
AppService.init(): Key = Specification-Version, Value = 9.0
So just by the above output, I think MANIFEST.MF is not application specific but is from commons-daemon.jar.
OK - the problem isn't that you "can't read data from META/MANIFEST.MF in Spring Boot MVC Web Application". Rather, the problem is that your code happens to be reading the WRONG MANIFEST.MF from some other, random .jar in the classpath.
One solution might be to use JarClassLoader.
Another solution, as M. Deinum suggested, might be to store the properties you wish to retrieve in application.properties (or some other "global" properties file) instead of MANIFEST.MF.
ALSO:
I assume you're probably using an IDE to develop your app (Eclipse, Netbeans, etc). If you haven't already, I would STRONGLY encourage you to familiarize yourself with your IDE's debugger: the ability to set breakpoints, display variables, single-step through method calls, etc.
In JEE environment it is useful to know, where a particular class is loaded from.
For example I have an instance of org.slf4j.Logger provided by a some black box library. Is it possible to find the responsible classloader? If the class of the instance comes from JDK, Application Server, EAR or Web Application classloader?
It turns out to be quite simple. The name of the classloader is returned by:
object.getClass().getClassLoader().getName()
It returns something like "app" or "platform". Be careful - classloader is null, if the class belongs to the bootstrap classloader, like the JUL logger java.util.logging.Logger does.
WebLogic has a long chain of classloaders without names. WebLogic's classloaders contain a useful field annotation. One can read it to find the JEE application, the classloader belongs to:
public Object getAnnotation(ClassLoader classloader) {
try {
Method amethod = classloader.getClass().getMethod("getAnnotation");
return amethod.invoke(classloader);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
return "";
}
}
If you want to print an overview of all available classloader without digging for annotations, the hashcode of the classloader instance can be used. Here is a small JSP page. Put it into the webapp directory of your web project to get a quick overview.
<%
response.setContentType("text/plain");
List<Class<?>> clazzes = List.of(Logger.class, object.getClass());
out.println("CLASSLOADERS:\n");
ClassLoader classloader = new Object() {}.getClass().getClassLoader();
while (classloader != null) {
out.println(
String.format("%-18s", classloader.getName()) + " " +
String.format("%-10s", Integer.toHexString(classloader.hashCode())) + " " +
classloader.getClass().getName() + " / " + getAnnotation(classloader));
classloader = classloader.getParent();
}
out.println("\nCLASSES:\n");
for (Class<?> clazz : clazzes) {
ClassLoader cloader = clazz.getClassLoader();
URL location = Optional.of(clazz.getProtectionDomain()).map(x->x.getCodeSource()).map(x->x.getLocation()).orElse(null);
out.println(
clazz + " \n " +
(cloader != null ? Integer.toHexString(cloader.hashCode()) : "<bootstrap>") + "\t" +
location);
}
%>
I wanted to retrieve values from my context.xml, and I've found this snippet of code to do so:
// Acquire an instance of our specified bean class
MyBean bean = new MyBean();
// Customize the bean properties from our attributes
Reference ref = (Reference) obj;
Enumeration addrs = ref.getAll();
while (addrs.hasMoreElements()) {
RefAddr addr = (RefAddr) addrs.nextElement();
String name = addr.getType();
String value = (String) addr.getContent();
if (name.equals("foo")) {
bean.setFoo(value);
} else if (name.equals("bar")) {
try {
bean.setBar(Integer.parseInt(value));
} catch (NumberFormatException e) {
throw new NamingException("Invalid 'bar' value " + value);
}
}
}
// Return the customized instance
return (bean);
I wanted to know if there was a method to do the exact same thing but with less steps
a web application on Tomcat 8.0
Tomcat 8.0 has reached End of Life. Do not use it. See "Migration Guide" at tomcat.apache.org to upgrade to Tomcat 8.5 or 9.0.
See "JDNI Resources" in Tomcat documentation. E.g. factory="org.apache.naming.factory.BeanFactory" can be used to create an arbitrary bean.
If you just need a set of configurable properties, defining them with "Parameter" element in Context will be easier. A web application will get those values via javax.servlet.ServletContext.getInitParameter(name) API.
When using a directory-expression for an <int-file:outbound-gateway> endpoint, the method below is called on org.springframework.integration.file.FileWritingMessageHandler:
private File evaluateDestinationDirectoryExpression(Message<?> message) {
final File destinationDirectory;
final Object destinationDirectoryToUse = this.destinationDirectoryExpression.getValue(
this.evaluationContext, message);
if (destinationDirectoryToUse == null) {
throw new IllegalStateException(String.format("The provided " +
"destinationDirectoryExpression (%s) must not resolve to null.",
this.destinationDirectoryExpression.getExpressionString()));
}
else if (destinationDirectoryToUse instanceof String) {
final String destinationDirectoryPath = (String) destinationDirectoryToUse;
Assert.hasText(destinationDirectoryPath, String.format(
"Unable to resolve destination directory name for the provided Expression '%s'.",
this.destinationDirectoryExpression.getExpressionString()));
destinationDirectory = new File(destinationDirectoryPath);
}
else if (destinationDirectoryToUse instanceof File) {
destinationDirectory = (File) destinationDirectoryToUse;
} else {
throw new IllegalStateException(String.format("The provided " +
"destinationDirectoryExpression (%s) must be of type " +
"java.io.File or be a String.", this.destinationDirectoryExpression.getExpressionString()));
}
validateDestinationDirectory(destinationDirectory, this.autoCreateDirectory);
return destinationDirectory;
}
Based on this code I see that if the directory to use evaluates to a String, it uses that String to create a new java.io.File object.
Is there a reason that a ResourceLoader couldn't/shouldn't be used instead of directly creating a new file?
I ask because my expression was evaluating to a String of the form 'file://path/to/file/' which of course is an invalid path for the java.io.File(String) constructor. I had assumed that Spring would treat the String the same way as it treats the directory attribute on <int-file:outbound-gateway> and pass it through a ResourceLoader.
Excerpt from my configuration file:
<int-file:outbound-gateway
request-channel="inputChannel"
reply-channel="updateTable"
directory-expression="
'${baseDirectory}'
+
T(java.text.MessageFormat).format('${dynamicPathPattern}', headers['Id'])
"
filename-generator-expression="headers.filename"
delete-source-files="true"/>
Where baseDirectory is a property that changes per-environment of the form 'file://hostname/some/path/'
There's no particular reason that this is the case, it probably just wasn't considered at the time of implementation.
The request sounds reasonable to me and will benefit others (even though you have found a work-around), by providing simpler syntax. Please open an 'Improvement' JIRA issue; thanks.
While not directly answering the question, I wanted to post the workaround that I used.
In my XML configuration, I changed the directory-expression to evaluate to a file through the DefaultResourceLoader instead of a String.
So this is what my new configuration looked like:
<int-file:outbound-gateway
request-channel="inputChannel"
reply-channel="updateTable"
directory-expression=" new org.springframework.core.io.DefaultResourceLoader().getResource(
'${baseDirectory}'
+
T(java.text.MessageFormat).format('${dynamicPathPattern}', headers['Id'])).getFile()
"
filename-generator-expression="headers.filename"
delete-source-files="true"/>
I have tried a ton of variants of the below to get datasources to work but to no avail. I have been researching/trying for a few days now so I'm throwing in the towel and asking for help. First off though, I am having a hard time formating my code in this post. Nothing is getting indented and certain xml tags are disappearing. Probably stupid IE that work forces us to use....
web.xml
<resource-ref>
<res-ref-name>jdbc/nalabor</res-ref-name>
<res-type>oracle.jdbc.pool.OracleDataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
<resource-ref>
<res-ref-name>jdbc/navarch</res-ref-name>
<res-type>oracle.jdbc.pool.OracleDataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
context.xml
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<Resource
name="jdbc/nalabor" type="oracle.jdbc.pool.OracleDataSource"
maxActive="1" maxIdle="1" maxWait="10000"
factory="oracle.jdbc.pool.OracleDataSourceFactory"
url="jdbc:oracle:thin:#####.com:1521:SID"
driverClassName="oracle.jdbc.driver.OracleDriver"
username="###" password="###"/>
<Resource
name="jdbc/navarch" type="oracle.jdbc.pool.OracleDataSource"
maxActive="1" maxIdle="1" maxWait="10000"
factory="oracle.jdbc.pool.OracleDataSourceFactory"
url="jdbc:oracle:thin:#####.com:1521:SID"
driverClassName="oracle.jdbc.driver.OracleDriver"
username="###" password="###"/>
</Context>
Dao
try {
Context initContext = new InitialContext();
NamingEnumeration list = initContext.list("java:/comp/env");
System.out.println("Listing NamingEnumeration For java:/comp/env");
while (list.hasMore()) {
NameClassPair nc = (NameClassPair)list.next();
System.out.println("Name Class Pair = " + nc);
}
list = initContext.list("java:/comp/env/jdbc");
System.out.println("Listing NamingEnumeration java:/comp/env/jdbc");
while (list .hasMore()) {
NameClassPair nc = (NameClassPair)list .next();
System.out.println("Name Class Pair = " + nc);
}
Context envContext = (Context) initContext.lookup("java:/comp/env");
ods = (OracleDataSource) envContext.lookup("jdbc/nalabor");
} catch (Exception ex) {
System.out.println("ERORRRRRRRR AGAIN!");
ex.printStackTrace();
}
Stack
Listing NamingEnumeration For java:/comp/env
Name Class Pair = mailClient: java.lang.String
Name Class Pair = siteConnCache: java.lang.String
Name Class Pair = jdbc: org.apache.naming.NamingContext
Name Class Pair = sitePOCEmail: java.lang.String
Name Class Pair = siteFilePrefix: java.lang.String
Name Class Pair = siteName: java.lang.String
Name Class Pair = siteEmail: java.lang.String
Listing NamingEnumeration java:/comp/env/jdbc
Name Class Pair = nalabor: org.apache.naming.ResourceRef
Name Class Pair = navarch: org.apache.naming.ResourceRef
ERORRRRRRRR AGAIN!
javax.naming.NamingException: Cannot create resource instance
at org.apache.naming.factory.ResourceFactory.getObjectInstance(ResourceFactory.java:167)
at javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:314)
at org.apache.naming.NamingContext.lookup(NamingContext.java:834)
at org.apache.naming.NamingContext.lookup(NamingContext.java:181)
at org.apache.naming.NamingContext.lookup(NamingContext.java:822)
at org.apache.naming.NamingContext.lookup(NamingContext.java:194)
at com.gdebI.rozycki.bsc.data.LaborDAO.getWeightedLabor(LaborDAO.java:91)
at com.gdebI.rozycki.bsc.controller.action.WeightedLabor.getList(WeightedLabor.java:66)
at com.gdebI.rozycki.controller.action.ListAction.service(ListAction.java:38)
WEB-INF/lib
ojdbc14.jar
I'm not sure why you listed your resources in your web.xml but I think you are including an extra / that is causing the problem. I've encountered this exception when the name can't be found. Try this (Java 6+):
OracleDataSource ods = InitialContext.doLookup("java:comp/env/jdbc/nalabor");
or this for Java 5 and below:
InitialContext ic = new InitialContext();
OracleDataSource ods = (OracleDataSource)ic.lookup("java:comp/env/jdbc/nalabor");
I meet this problem and I solve it . notice your the order of elements in your web.xml. I resort the order of element in My web app's web.xml as the http://wiki.apache.org/tomcat/Specifications says and it works.