Tomcat: rewrite your urls without any library!
Rewriting application URLs is quite common. This is due to a lot of causes like specification evolution (/api sould now be /api/v1), environment marking (/api becoming /prod/api or /preprod/api) etc...
The common point of these needs is generally it happens outside the application/development cycle.
Most of the time it is solved by the proxy layer - ngnix, httpd, ... - but Tomcat (so TomEE and Meecrowave) supports out of the box a RewriteValve allowing to fulfill these needs very quickly without any other need.
The (tomcat) rewrite syntax
The overall idea is to configure some httpd like rules in a rewrite.config located in your war in WEB-INF folder or your host configuration folder in tomcat conf/ folder. It allows mainly local remapping or urls or plain HTTP redirections.
Here is an example of a HTTP redirection (/google/foo to https://google.com/search?q=foo):
RewriteRule ^/google/(.*)$ https://google.com/search?q=$1 [R,NE]
And here is a local redirection (/local/foo to /api/foo):
RewriteRule ^/local/(.*)$ /api/$1 [L,NE]
It also supports some condition on hosts/headers/...For instance if you only want to redirect when request happens on localhost you can do:
RewriteCond %{REMOTE_HOST} ^127.0.0.1$
RewriteRule ^/local/(.*)$ /api/$1 [L,NE]
Etc...
I'll not detail all the syntax but you can find most of it at https://tomcat.apache.org/tomcat-9.0-doc/rewrite.html.
Now we quickly saw how we can use it let's see how to configure the valve.
Set up RewriteValve
This is as easy as setting up a standard tomcat valve but ensure to configure it on the container you put the configuration in. Concretely if you put the configuration in WEB-INF ensure to configure it on your Context either through server.xml or context.xml:
<Valve className="org.apache.catalina.valves.rewrite.RewriteValve" />
This is simple but enforces you to reuse this rewrite.config file.
Hot reload support with Meecrowave
You can go a bit further coding a bit around the valve. Typically with Meecrowave you can read the config from where you want (including some database) and just set it directly on the valve:
public class ProxyCustomizer implements Meecrowave.ConfigurationCustomizer, Cli.Options {
@CliOption(name = "proxy-configuration", description = "Where to read the proxy rules from")
private File proxyConfiguration = new File("src/main/resources/proxy.rewrite");
@Override
public void accept(final Meecrowave.Builder builder) {
// 1
final ProxyCustomizer extension = builder.getExtension(ProxyCustomizer.class);
// 2
final String conf;
try {
conf = Files.readAllLines(extension.proxyConfiguration.toPath()).stream()
.collect(Collectors.joining("\n"));
} catch (final IOException e) {
throw new IllegalStateException(e);
}
// 3
final RewriteValve proxy = new RewriteValve() {
@Override
protected synchronized void startInternal() throws LifecycleException {
super.startInternal();
try {
setConfiguration(conf);
} catch (final Exception e) {
throw new LifecycleException(e);
}
}
};
// 4
builder.instanceCustomizer(tomcat -> tomcat.getHost().getPipeline().addValve(proxy));
}
}
- this is a meecrowave way to get back our proxy configuration, you can replace it by any way fitting your case
- we read all the file we configured, idea there is to have all the rules in memory in conf variable
- we extend the valve to set our configuration programmatically through setConfiguration()
- we add our valve to the host programmatically
Tip: this was done using a configuration customizer of Meecrowave but you can also do it at runtime. The only trick will be to ensure you didnt get any request yet cause the setConfiguration() method is not thread safe. If you manage to make it thread safe (protexting the valve with a lock for instance) you can even update the configuration at runtime getting a dynamic local proxy!
Here is a thread safe version:
final RewriteValve proxy = new RewriteValve() {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
@Override
public void invoke(final Request request, final Response response) throws IOException, ServletException {
final Lock lock = this.lock.readLock();
lock.lock();
try {
super.invoke(request, response);
} finally {
lock.unlock();
}
}
@Override
public void setConfiguration(final String configuration) throws Exception {
final Lock lock = this.lock.writeLock();
lock.lock();
try {
super.setConfiguration(configuration);
} finally {
lock.unlock();
}
}
};
And here how to update it through a scheduled task for instance:
@ApplicationScoped
public class ProxyUpdater {
private Runnable shutdownHook;
// 1
@Inject
private Meecrowave.Builder config;
//2
public void init(@Observes @Initialized(ApplicationScoped.class) final ServletContext servletContext) {
final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
final ScheduledFuture<?> task = scheduledExecutorService.scheduleAtFixedRate(this::update, 0, 5, TimeUnit.MINUTES);
shutdownHook = () -> {
task.cancel(true);
scheduledExecutorService.shutdownNow();
};
}
@PostConstruct
private void stop() {
ofNullable(shutdownHook).ifPresent(Runnable::run);
}
// 3
private void update() {
try {
config.getExtension(ProxyCustomizer.class).getProxy().setConfiguration(loadConfiguration());
} catch (Exception e) {
// log etc...
}
}
private String loadConfiguration() {
return null; // read from sb
}
}
- to update regularly the configuration we need to have access to the valve, here the trick is to store the previous valve instance in a meecrowave extension and read it from the extension, any alternative can work like reading it from JMX or using a Tomcat listener to capture it. This one has the advantage to stay simple but relies on Meecrowave configuration/extension API.
- we create a task execute each 5mn. Quick side note is using a runnable as shutdown task is a pattern enabled by Java 8 which makes code easier to write and handle for such needs IMHO.
- we implement the actual reload and get the valve instance (getProxy()) and re-set the configuration.
Concretely our customize now looks like:
public class ProxyCustomizer implements Meecrowave.ConfigurationCustomizer, Cli.Options {
@CliOption(name = "proxy-configuration", description = "Where to read the proxy rules from")
private File proxyConfiguration = new File("src/main/resources/proxy.rewrite");
private RewriteValve proxy;
@Override
public void accept(final Meecrowave.Builder builder) {
final ProxyCustomizer extension = builder.getExtension(ProxyCustomizer.class);
extension.proxy = new RewriteValve() {
// as before
};
builder.instanceCustomizer(tomcat -> tomcat.getHost().getPipeline().addValve(extension.proxy));
}
public RewriteValve getProxy() {
return proxy;
}
}
Conclusion
Even if Tomcat RewriteValve will not replace your httpd for multiple reasons, it can be a smooth way to solve small deployment mapping issues we hit from time to time when going in preprod/prod environments and doesn't require any intermediate layer/proxy or knowledge. Being java based, it is very simple to extend it to something more powerful and with a little bit of effort you can even redefine the parsing rules to match some custom needs. The main missing feature to make of it a real proxy is the remote proxying support (it is a TODO in the code) but for such a need it is easy to import camel and implement it based on camel instead of the RewriteValve which would need another library like httpclient to do it properly.
From the same author:
In the same category: