From eced4cccce5775b3e954202e2d65e9ea71dddf5a Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Tue, 15 Sep 2020 10:47:07 -0500 Subject: [PATCH] Issue #5032 - WebAppContext & Configuration hooks added Signed-off-by: Joakim Erdfelt --- jetty-metrics/pom.xml | 6 + .../metrics/MetricsConfigurationWrapper.java | 75 ++++++++ .../eclipse/jetty/metrics/MetricsHandler.java | 27 +++ .../jetty/metrics/WebAppMetricsListener.java | 15 +- .../eclipse/jetty/metrics/HelloServlet.java | 2 + .../ServletContextHandlerMetricsTest.java | 4 +- .../ServletMetricsCaptureListener.java | 9 +- .../metrics/WebAppContextMetricsTest.java | 175 ++++++++++++++++++ .../metrics/WebAppMetricsCaptureListener.java | 49 +++++ .../eclipse/jetty/webapp/Configuration.java | 56 ++++++ .../eclipse/jetty/webapp/WebAppContext.java | 15 +- 11 files changed, 424 insertions(+), 9 deletions(-) create mode 100644 jetty-metrics/src/main/java/org/eclipse/jetty/metrics/MetricsConfigurationWrapper.java create mode 100644 jetty-metrics/src/test/java/org/eclipse/jetty/metrics/WebAppContextMetricsTest.java create mode 100644 jetty-metrics/src/test/java/org/eclipse/jetty/metrics/WebAppMetricsCaptureListener.java diff --git a/jetty-metrics/pom.xml b/jetty-metrics/pom.xml index 1c616c1df78c..0bc4a6ff336a 100644 --- a/jetty-metrics/pom.xml +++ b/jetty-metrics/pom.xml @@ -58,5 +58,11 @@ ${project.version} test + + org.eclipse.jetty + jetty-annotations + ${project.version} + test + diff --git a/jetty-metrics/src/main/java/org/eclipse/jetty/metrics/MetricsConfigurationWrapper.java b/jetty-metrics/src/main/java/org/eclipse/jetty/metrics/MetricsConfigurationWrapper.java new file mode 100644 index 000000000000..5e7a2cabab08 --- /dev/null +++ b/jetty-metrics/src/main/java/org/eclipse/jetty/metrics/MetricsConfigurationWrapper.java @@ -0,0 +1,75 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.metrics; + +import org.eclipse.jetty.webapp.Configuration; +import org.eclipse.jetty.webapp.WebAppContext; + +public class MetricsConfigurationWrapper extends Configuration.Wrapper +{ + private final WebAppMetricsListener metricsListener; + + public MetricsConfigurationWrapper(Configuration configuration, WebAppMetricsListener metricsListener) + { + super(configuration); + this.metricsListener = metricsListener; + } + + @Override + public void preConfigure(WebAppContext context) throws Exception + { + try + { + metricsListener.onWebAppConfigureStart(context, getWrapped(), WebAppMetricsListener.ConfigurationStep.PRE); + super.preConfigure(context); + } + finally + { + metricsListener.onWebAppConfigureFinished(context, getWrapped(), WebAppMetricsListener.ConfigurationStep.PRE); + } + } + + @Override + public void configure(WebAppContext context) throws Exception + { + try + { + metricsListener.onWebAppConfigureStart(context, getWrapped(), WebAppMetricsListener.ConfigurationStep.MAIN); + super.configure(context); + } + finally + { + metricsListener.onWebAppConfigureFinished(context, getWrapped(), WebAppMetricsListener.ConfigurationStep.MAIN); + } + } + + @Override + public void postConfigure(WebAppContext context) throws Exception + { + try + { + metricsListener.onWebAppConfigureStart(context, getWrapped(), WebAppMetricsListener.ConfigurationStep.POST); + super.postConfigure(context); + } + finally + { + metricsListener.onWebAppConfigureFinished(context, getWrapped(), WebAppMetricsListener.ConfigurationStep.POST); + } + } +} diff --git a/jetty-metrics/src/main/java/org/eclipse/jetty/metrics/MetricsHandler.java b/jetty-metrics/src/main/java/org/eclipse/jetty/metrics/MetricsHandler.java index c831d5b5229c..c31f8b3f779a 100644 --- a/jetty-metrics/src/main/java/org/eclipse/jetty/metrics/MetricsHandler.java +++ b/jetty-metrics/src/main/java/org/eclipse/jetty/metrics/MetricsHandler.java @@ -36,12 +36,14 @@ import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.webapp.Configuration; import org.eclipse.jetty.webapp.WebAppContext; public class MetricsHandler extends ContainerLifeCycle implements ServletHolder.WrapperFunction, FilterHolder.WrapperFunction, ListenerHolder.WrapperFunction, + Configuration.WrapperFunction, HttpChannel.Listener, LifeCycle.Listener { @@ -129,6 +131,31 @@ public void onRequestBegin(Request request) request.setAttribute(ATTR_REQUEST_ID, uniqId); } + @Override + public Configuration wrapConfiguration(Configuration configuration) + { + LOG.info("wrapConfiguration({})", configuration); + if (!(metricsListener instanceof WebAppMetricsListener)) + { + return configuration; + } + + Configuration unwrapped = configuration; + while (unwrapped instanceof Configuration.Wrapper) + { + // Are we already wrapped somewhere along the line? + if (unwrapped instanceof MetricsConfigurationWrapper) + { + // If so, we are done. no need to wrap again. + return configuration; + } + // Unwrap + unwrapped = ((Configuration.Wrapper)unwrapped).getWrapped(); + } + + return new MetricsConfigurationWrapper(configuration, (WebAppMetricsListener)metricsListener); + } + @Override public EventListener wrapEventListener(EventListener listener) { diff --git a/jetty-metrics/src/main/java/org/eclipse/jetty/metrics/WebAppMetricsListener.java b/jetty-metrics/src/main/java/org/eclipse/jetty/metrics/WebAppMetricsListener.java index 2a2cc26be046..de7d49020b90 100644 --- a/jetty-metrics/src/main/java/org/eclipse/jetty/metrics/WebAppMetricsListener.java +++ b/jetty-metrics/src/main/java/org/eclipse/jetty/metrics/WebAppMetricsListener.java @@ -18,7 +18,6 @@ package org.eclipse.jetty.metrics; -import java.time.Duration; import javax.servlet.ServletContext; import org.eclipse.jetty.webapp.Configuration; @@ -32,14 +31,22 @@ enum ConfigurationStep } /** - * Timing for a specific {@link Configuration} being applied to the {@link WebAppContext} + * Event that a specific {@link Configuration} being applied to the {@link WebAppContext} * * @param context the specific context that was the configuration was applied to * @param configuration the configuration that was applied * @param configurationStep the configuration step - * @param duration the duration for this configuration step */ - void onWebAppConfigureTiming(WebAppContext context, Configuration configuration, ConfigurationStep configurationStep, Duration duration); + void onWebAppConfigureStart(WebAppContext context, Configuration configuration, ConfigurationStep configurationStep); + + /** + * Event that a specific {@link Configuration} being applied to the {@link WebAppContext} has completed + * + * @param context the specific context that was the configuration was applied to + * @param configuration the configuration that was applied + * @param configurationStep the configuration step + */ + void onWebAppConfigureFinished(WebAppContext context, Configuration configuration, ConfigurationStep configurationStep); /** * Event that the WebAppContext has started to be initialized diff --git a/jetty-metrics/src/test/java/org/eclipse/jetty/metrics/HelloServlet.java b/jetty-metrics/src/test/java/org/eclipse/jetty/metrics/HelloServlet.java index 9f14b04c6650..3e32314a387d 100644 --- a/jetty-metrics/src/test/java/org/eclipse/jetty/metrics/HelloServlet.java +++ b/jetty-metrics/src/test/java/org/eclipse/jetty/metrics/HelloServlet.java @@ -19,10 +19,12 @@ package org.eclipse.jetty.metrics; import java.io.IOException; +import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +@WebServlet(urlPatterns = "/hello") public class HelloServlet extends HttpServlet { @Override diff --git a/jetty-metrics/src/test/java/org/eclipse/jetty/metrics/ServletContextHandlerMetricsTest.java b/jetty-metrics/src/test/java/org/eclipse/jetty/metrics/ServletContextHandlerMetricsTest.java index d01bae081bcc..705d2abd14d7 100644 --- a/jetty-metrics/src/test/java/org/eclipse/jetty/metrics/ServletContextHandlerMetricsTest.java +++ b/jetty-metrics/src/test/java/org/eclipse/jetty/metrics/ServletContextHandlerMetricsTest.java @@ -88,7 +88,7 @@ public void testSimpleServlet() throws Exception expectedEvents.add("onServletEnter()"); expectedEvents.add("onServletExit()"); - assertThat("Metrics Events Count", captureListener.events.size(), is(expectedEvents.size())); + assertThat("Metrics Events Count", captureListener.getEvents().size(), is(expectedEvents.size())); } @Test @@ -122,6 +122,6 @@ public void testSimpleFilterAndServlet() throws Exception expectedEvents.add("onServletExit()"); expectedEvents.add("onFilterExit()"); - assertThat("Metrics Events Count", captureListener.events.size(), is(expectedEvents.size())); + assertThat("Metrics Events Count", captureListener.getEvents().size(), is(expectedEvents.size())); } } diff --git a/jetty-metrics/src/test/java/org/eclipse/jetty/metrics/ServletMetricsCaptureListener.java b/jetty-metrics/src/test/java/org/eclipse/jetty/metrics/ServletMetricsCaptureListener.java index ecdd0f1273d7..3c27beacc8ec 100644 --- a/jetty-metrics/src/test/java/org/eclipse/jetty/metrics/ServletMetricsCaptureListener.java +++ b/jetty-metrics/src/test/java/org/eclipse/jetty/metrics/ServletMetricsCaptureListener.java @@ -31,9 +31,9 @@ public class ServletMetricsCaptureListener implements ServletMetricsListener { private static final Logger LOG = Log.getLogger(ServletMetricsCaptureListener.class); - public LinkedBlockingQueue events = new LinkedBlockingQueue<>(); + private LinkedBlockingQueue events = new LinkedBlockingQueue<>(); - private void addEvent(String format, Object... args) + protected void addEvent(String format, Object... args) { String eventText = String.format(format, args); events.offer(eventText); @@ -46,6 +46,11 @@ private void addEvent(String format, Object... args) LOG.info("[EVENT] {}", eventText, cause); } + public LinkedBlockingQueue getEvents() + { + return events; + } + @Override public void onServletContextStarting(ServletContext servletContext) { diff --git a/jetty-metrics/src/test/java/org/eclipse/jetty/metrics/WebAppContextMetricsTest.java b/jetty-metrics/src/test/java/org/eclipse/jetty/metrics/WebAppContextMetricsTest.java new file mode 100644 index 000000000000..e912c0bf7e87 --- /dev/null +++ b/jetty-metrics/src/test/java/org/eclipse/jetty/metrics/WebAppContextMetricsTest.java @@ -0,0 +1,175 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.metrics; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jetty.annotations.AnnotationConfiguration; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.resource.PathResource; +import org.eclipse.jetty.webapp.Configuration; +import org.eclipse.jetty.webapp.FragmentConfiguration; +import org.eclipse.jetty.webapp.JettyWebXmlConfiguration; +import org.eclipse.jetty.webapp.MetaInfConfiguration; +import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.webapp.WebInfConfiguration; +import org.eclipse.jetty.webapp.WebXmlConfiguration; +import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; + +@ExtendWith(WorkDirExtension.class) +public class WebAppContextMetricsTest +{ + public WorkDir workDir; + private Server server; + private HttpClient client; + + @BeforeEach + public void setUp() throws Exception + { + server = new Server(); + ServerConnector connector = new ServerConnector(server); + connector.setPort(0); + server.addConnector(connector); + + client = new HttpClient(); + client.start(); + } + + @AfterEach + public void tearDown() + { + LifeCycle.stop(client); + LifeCycle.stop(server); + } + + private void enableByteCodeScanning() + { + Configuration.ClassList classlist = Configuration.ClassList + .setServerDefault(server); + + classlist.addBefore( + "org.eclipse.jetty.webapp.JettyWebXmlConfiguration", + "org.eclipse.jetty.annotations.AnnotationConfiguration"); + } + + @Test + public void testBasicBytecodeScan() throws Exception + { + Path webappDir = workDir.getEmptyPathDir(); + Path webinfDir = webappDir.resolve("WEB-INF"); + Path classesDir = webinfDir.resolve("classes"); + FS.ensureDirExists(classesDir); + copyClassToWebInfClasses(HelloServlet.class, classesDir); + + enableByteCodeScanning(); + + WebAppMetricsCaptureListener captureListener = new WebAppMetricsCaptureListener(); + MetricsHandler metricsHandler = new MetricsHandler(captureListener); + + WebAppContext webapp = new WebAppContext(); + webapp.setContextPath("/"); + webapp.addBean(metricsHandler); + webapp.setWarResource(new PathResource(webappDir)); + + metricsHandler.addToAllConnectors(server); + metricsHandler.addToContext(webapp); + server.setHandler(webapp); + server.start(); + + ContentResponse response = client.GET(server.getURI().resolve("/hello")); + assertThat("Response.status", response.getStatus(), is(HttpStatus.OK_200)); + + Class[] configurationClasses = { + WebInfConfiguration.class, + WebXmlConfiguration.class, + MetaInfConfiguration.class, + FragmentConfiguration.class, + AnnotationConfiguration.class, + JettyWebXmlConfiguration.class + }; + + List expectedEvents = new ArrayList<>(); + expectedEvents.add("onWebAppStarting()"); + expectedEvents.add("onServletContextStarting()"); + addExpectedConfigurations(expectedEvents, WebAppMetricsListener.ConfigurationStep.PRE, configurationClasses); + addExpectedConfigurations(expectedEvents, WebAppMetricsListener.ConfigurationStep.MAIN, configurationClasses); + expectedEvents.add("onServletStarting() - DefaultServlet"); + expectedEvents.add("onServletReady() - DefaultServlet"); + expectedEvents.add("onServletStarting() - NoJspServlet"); + expectedEvents.add("onServletReady() - NoJspServlet"); + addExpectedConfigurations(expectedEvents, WebAppMetricsListener.ConfigurationStep.POST, configurationClasses); + expectedEvents.add("onWebAppReady()"); + expectedEvents.add("onServletContextReady()"); + expectedEvents.add("onServletStarting() - HelloServlet"); + expectedEvents.add("onServletReady() - HelloServlet"); + expectedEvents.add("onServletEnter()"); + expectedEvents.add("onServletExit()"); + + assertThat("Metrics Events Count", captureListener.getEvents().size(), is(expectedEvents.size())); + } + + private void addExpectedConfigurations(List expectedEvents, WebAppMetricsListener.ConfigurationStep configStep, Class[] configurationClasses) + { + for (Class configClass : configurationClasses) + { + expectedEvents.add(String.format("onWebAppConfigureStart() - %s:%s", configStep, configClass.getName())); + expectedEvents.add(String.format("onWebAppConfigureFinished() - %s:%s", configStep, configClass.getName())); + } + } + + private void copyClassToWebInfClasses(Class clazz, Path classesDir) throws IOException + { + String classRef = clazz.getName().replace('.', '/') + ".class"; + URL url = this.getClass().getResource('/' + classRef); + MatcherAssert.assertThat("URL to class " + classRef, url, is(not(nullValue()))); + Path destPath = classesDir.resolve(classRef); + FS.ensureDirExists(destPath.getParent()); + try (InputStream in = url.openStream(); + OutputStream out = Files.newOutputStream(destPath)) + { + IO.copy(in, out); + } + } +} + diff --git a/jetty-metrics/src/test/java/org/eclipse/jetty/metrics/WebAppMetricsCaptureListener.java b/jetty-metrics/src/test/java/org/eclipse/jetty/metrics/WebAppMetricsCaptureListener.java new file mode 100644 index 000000000000..09df63e571f1 --- /dev/null +++ b/jetty-metrics/src/test/java/org/eclipse/jetty/metrics/WebAppMetricsCaptureListener.java @@ -0,0 +1,49 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.metrics; + +import org.eclipse.jetty.webapp.Configuration; +import org.eclipse.jetty.webapp.WebAppContext; + +public class WebAppMetricsCaptureListener extends ServletMetricsCaptureListener implements WebAppMetricsListener +{ + @Override + public void onWebAppConfigureStart(WebAppContext context, Configuration configuration, ConfigurationStep configurationStep) + { + addEvent("onWebAppConfigureStart(), configuration=%s, configurationStep=%s, context=%s", configuration, configurationStep, context); + } + + @Override + public void onWebAppConfigureFinished(WebAppContext context, Configuration configuration, ConfigurationStep configurationStep) + { + addEvent("onWebAppConfigureFinished(), configuration=%s, configurationStep=%s, context=%s", configuration, configurationStep, context); + } + + @Override + public void onWebAppStarting(WebAppContext context) + { + addEvent("onWebAppStarting(), context=%s", context); + } + + @Override + public void onWebAppReady(WebAppContext context) + { + addEvent("onWebAppReady(), context=%s", context); + } +} diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/Configuration.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/Configuration.java index aba1a28b8e9c..14e8eea06ac9 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/Configuration.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/Configuration.java @@ -96,6 +96,62 @@ public interface Configuration */ void cloneConfigure(WebAppContext template, WebAppContext context) throws Exception; + interface WrapperFunction + { + Configuration wrapConfiguration(Configuration configuration); + } + + class Wrapper implements Configuration + { + private Configuration delegate; + + public Wrapper(Configuration delegate) + { + this.delegate = delegate; + } + + public Configuration getWrapped() + { + return delegate; + } + + @Override + public void preConfigure(WebAppContext context) throws Exception + { + delegate.preConfigure(context); + } + + @Override + public void configure(WebAppContext context) throws Exception + { + delegate.configure(context); + } + + @Override + public void postConfigure(WebAppContext context) throws Exception + { + delegate.postConfigure(context); + } + + @Override + public void deconfigure(WebAppContext context) throws Exception + { + delegate.deconfigure(context); + } + + @Override + public void destroy(WebAppContext context) throws Exception + { + delegate.destroy(context); + } + + @Override + public void cloneConfigure(WebAppContext template, WebAppContext context) throws Exception + { + delegate.cloneConfigure(template, context); + } + } + class ClassList extends ArrayList { diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java index 14df9557bd68..616958e636e5 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java @@ -980,10 +980,23 @@ protected void loadConfigurations() } for (String configClass : _configurationClasses) { - _configurations.add((Configuration)Loader.loadClass(configClass).getDeclaredConstructor().newInstance()); + @SuppressWarnings("unchecked") + Configuration configuration = (Configuration)Loader.loadClass(configClass).getDeclaredConstructor().newInstance(); + configuration = wrap(configuration); + _configurations.add(configuration); } } + private Configuration wrap(final Configuration configuration) + { + Configuration ret = configuration; + for (Configuration.WrapperFunction wrapperFunction : getBeans(Configuration.WrapperFunction.class)) + { + ret = wrapperFunction.wrapConfiguration(ret); + } + return ret; + } + @Override public String toString() {