How do you save name-value pairs in Tomcat environment?
We have a servlet that needs certain variables like passwords, encryption salts, etc., not to be saved on the file system permanently. This is what we do currently (summary):
During initialization,
A Perl script sets ReadMode to 2 to mask stdout echo, prompts user开发者_如何学JAVA for the variables, filters a known file to put them in and invokes tomcat/bin/startup.sh
The servlet init() method reads the variables from the file and deletes it (the file).
Problem: When the WAR is recompiled, tomcat tries to deploy it (autodeploy=true), which we want. But the data file is no longer there, so a FileNotFoundException is thrown (rightly so).
Question: Is there a property or some HashMap/Table available to servlets where a few variables can be stored during the manual startup? The idea is that init() could check for them if the data file is not there during redeployment. Thank you, - MS.
How about passing them in as system properties when you call startup.sh, ie. 'bin/startup.sh -DencryptionSalt=foobar'? Then you can call System.getProperty("encryptionSalt") for the duration of the JVM.
Put your volatile data into JNDI. JNDI isn't cleaned up between redeployments. Your servlet could still do the same thing during init
to ensure the data in JNDI is up-to-date.
I can't remember if JNDI reads/writes are thread safe, but it it isn't, you can always put a thread-safe object (e.g. ConcurrentHashMap
) and use it without problems.
EDIT by MS:
==========
restart.pl:
==========
ReadMode 2; print "\nEnter spice: "; chomp ($spice = <STDIN>); print "\n";
ReadMode 0;
open (FP, ">$spicefile") or die "$0: Cannot write file $spicefile: $!\n";
print FP "$spice\n"; close (FP);
system "bin/shutdown.sh"; sleep 8;
system "bin/startup.sh"; sleep 8; system "wget $tomcaturl";
system '/bin/rm -rf *spicefile*'; # delete wget output file
foreach $sCount (1..10) { # give it 10 more secs
sleep 1;
if (-f $spicefile) {
print "$0: Waiting on servlet to delete spicefile [$sCount]\n"
} else {
print "$0: Successful servlet initialization, no spicefile\n";
exit 0
}}
print "\n$0: deleting file $spicefile ...\n"; # Error condition
system "unlink $spicefile";
exit 1;
===========
context.xml
===========
<Resource name="MyServlet/upsBean" auth="Container" type="packageName.UPSBean"
factory="org.apache.naming.factory.BeanFactory" readOnly="false"/>
===================
app/WEB-INF/web.xml
===================
<listener>
<listener-class>packageName.DBInterface</listener-class>
</listener>
<resource-env-ref>
<description>Use spice as transferable during redeployment</description>
<resource-env-ref-name>MyServlet/upsBean</resource-env-ref-name>
<resource-env-ref-type>packageName.UPSBean</resource-env-ref-type>
</resource-env-ref>
==============
MyServlet.java:
==============
init() {
if (new File (spiceFile).exists()) # Same name in restart.pl
dbi = new DBInterface (spiceFile);
else
dbi = new DBInterface (); # Redeployment
otherInitializations (dbi.getSpice());
}
================
DBInterface.java:
================
public DBInterface () {
// Comment out following block if contextInitialized works
FileInputStream fin = new FileInputStream (safeDepositBox);
ObjectInputStream ois = new ObjectInputStream (fin);
UPSBean upsBean = (UPSBean) ois.readObject();
ois.close();
spice = upsBean.getSpice();
dbiIndex = 2;
// do stuff with spice
}
public DBInterface (String spiceFileName) {
File file = new File (spiceFileName);
BufferedReader br = new BufferedReader (new FileReader (file));
spice = br.readLine();
br.close();
file.delete();
dbiIndex = 1;
// Delete following block if contextInitialized works
UPSBean upsBean = new UPSBean();
upsBean.setSpice (spice);
FileOutputStream fout = new FileOutputStream (safeDepositBox);
ObjectOutputStream oos = new ObjectOutputStream (fout);
oos.writeObject (upsBean);
oos.flush();
oos.close();
// do stuff with spice and if it works, ...
// contextInitialized (null);
}
// Above is working currently, would like the following to work
public void contextDestroyed(ServletContextEvent sce) {
System.setProperty ("spice", spice);
System.out.println ("[DBInterface" + dbiIndex +
"] Spice saved at " +
DateFormat.getDateTimeInstance (DateFormat.SHORT,
DateFormat.LONG).format (new Date()));
}
public void contextInitialized(ServletContextEvent sce) {
if (sce != null) {
spice = System.getProperty ("spice");
System.out.println ("[DBInterface" + dbiIndex +
"] Spice retrieved at " +
DateFormat.getDateTimeInstance (DateFormat.SHORT,
DateFormat.LONG).format (new Date()));
}
// do stuff with spice
}
============
UPSBean.java:
============
public class UPSBean implements Serializable {
private String spice = "parsley, sage, rosemary and thyme";
public UPSBean() { }
public String getSpice() { return spice; }
public void setSpice (String s) { spice = s; }
}
I am trying to see if get/setProperty works above. Was trying to use JNDI directly, but when I setSpice using the resource MyServlet/upsBean in contextDestroyed() and try to read it in contextInitialized(), I get a null (Sorry, I already deleted that part of the code). Now the declaration in context.xml as well as resource-env-ref has become redundant. Current workaround is to save the serialized instance in a data file (not very good).
There is nothing in Tomcat to do what you want. If you move the settings into the tomcat JNDI tree you will have to put the user/name password combo in the server.xml or the context.xml file. Here are a couple of possible solutions for the problem you have.
Option 1: Use a Tomcat Listener
If you look at the top of the tomcat server.xml file you will see several listeners these are java classes that execute when tomcat starts up. You can create you own tomcat listener which reads the password from the file system, deletes the file and stores the username/password Comobo in a way that is accessible to the app. The tomcat listener is tied to the lifecyle of the tomcat server so auto redeploy of the app will not cause your tomcat listener class to be reloaded. The code for your tomcat listener would have to be put into a jar and place in the CATALINA_HOME\lib folder.
The listener could use static variables to store the username/password and have a static methods that return them, this will work because the listener will be in the parent class loader of the tomcat apps. The disadvantages of this approach is that you application code is dependent on the tomcat resource listener implementation, and in your unit tests you might have to do some extra steps to make sure your code can be unit tested.
The listener could also access the tomcat global JNDI tree and put the username and password combo in there, and then your app would have to have context.xml file use a ResourceLink element to make the global jndi entry available to the app. You will also have to do some extra work to make this approach work with unit tests as looking stuff up in JNDI generally complicates unit testing.
If you are using Spring on this project your best bet is to use the static variables in a tomcat listener but then use a custom spring scope to pull the data out of the tomcat listener. that way your app stays testable and you can inject username/password combos into any piece of code that needs them.
Option 2: Use a Servlet Context Listener
In this option you write a Context Listener which will allow your app to be notified whenever the app starts and stops. With this approach on startup the context listener will run and read the password information and delete the file. If the password file is not there on startup then the context listener would have to have a way to get the admin to regenerate the file.
Option 3: Use JMX
Create a JMX MBean register it with the JVM MBeanServer and then use it to store the username/password combo. If you initialize this MBean from a tomcat listener you could then have the perl script call the MBean remotely and pass in the username/password combo.
Hope this helps.
If you want to handle it programmatically, I think what you might be looking for is a ServletContextListener. Create a class that implements the interface and code the needed functionality in the contextInitialized(ServletContextEvent sce) -method, see here for simple example.
Tomcat provides nothing to help you here.
Thats the bad news.
The good news is ... this is a process issue which can be solved using the tools the servlet-api provides as well as some additional items Tomcat can provide. Here are the ingredients ..
- Tomcat provides on container startup the the ability to have Listener - via the Listener element. Use these to track when the container starts up, shuts down, etc.
- The servlet api provides ServletContextListener to listen for webapp startup, shutdown
- When tomcat starts up - you may pass in System properties by first setting JAVA_OPTS. But be careful - these values can be discovered with the ps command.
- Depending on your deployment method - you can add settings for your configuration via my-app.xml (where my-app is the name of the app for deployment)
- In CATALINA_BASE/conf/context.xml - you may also set init/env params for the webapp to use for ALL webapps to see.
- If you are NOT using a security manager - you should be able to set/clear some system properties.
Given all of the above - you could read the "protected" values from a file and write them to a system property. Then for added protection in case you don't want the values always existing as System properties - you can utilize ServletContextListener to read the system property values and delete them from system properties. Then on re-deployment - the listener would write reset the system properties on shutdown - so when the webapp restarts up - they are still there.
So the startup sequence would be like this
- Admin enters pw (existing process) and the file is saved
- On webapp startup - ServletContextListner looks for file. If exists - uses those values
- On webapp shutdown - writes the values to System.properties
- On webapp restart - ServletContextListner sees the file is missing and uses the System.properties. Then deletes the system.properties
With the above - you can redeploy while not writing the passwords to disk and hopefully minimizing any attack vectors by temporarily storing as system.properties.
Good luck.
精彩评论