/* * Copyright 2016-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.glowroot.agent.plugin.spring; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.annotation.Nullable; import org.glowroot.agent.plugin.api.Agent; import org.glowroot.agent.plugin.api.MessageSupplier; import org.glowroot.agent.plugin.api.ThreadContext; import org.glowroot.agent.plugin.api.ThreadContext.Priority; import org.glowroot.agent.plugin.api.TimerName; import org.glowroot.agent.plugin.api.TraceEntry; import org.glowroot.agent.plugin.api.config.BooleanProperty; import org.glowroot.agent.plugin.api.util.FastThreadLocal; import org.glowroot.agent.plugin.api.weaving.BindMethodMeta; import org.glowroot.agent.plugin.api.weaving.BindParameter; import org.glowroot.agent.plugin.api.weaving.BindThrowable; import org.glowroot.agent.plugin.api.weaving.BindTraveler; import org.glowroot.agent.plugin.api.weaving.OnAfter; import org.glowroot.agent.plugin.api.weaving.OnBefore; import org.glowroot.agent.plugin.api.weaving.OnReturn; import org.glowroot.agent.plugin.api.weaving.OnThrow; import org.glowroot.agent.plugin.api.weaving.Pointcut; import org.glowroot.agent.plugin.api.weaving.Shim; // TODO optimize away servletPath thread local, e.g. store servlet path in thread context via // servlet plugin and retrieve here public class ControllerAspect { private static final FastThreadLocal</*@Nullable*/ String> servletPath = new FastThreadLocal</*@Nullable*/ String>(); private static final BooleanProperty useAltTransactionNaming = Agent.getConfigService("spring").getBooleanProperty("useAltTransactionNaming"); private static final ConcurrentMap<String, String> normalizedPatterns = new ConcurrentHashMap<String, String>(); @Shim("javax.servlet.http.HttpServletRequest") public interface HttpServletRequest { @Nullable String getContextPath(); @Nullable String getServletPath(); @Nullable String getPathInfo(); } @Shim("org.springframework.web.servlet.mvc.method.RequestMappingInfo") public interface RequestMappingInfo { @Shim("org.springframework.web.servlet.mvc.condition.PatternsRequestCondition" + " getPatternsCondition()") @Nullable PatternsRequestCondition glowroot$getPatternsCondition(); } @Shim("org.springframework.web.servlet.mvc.condition.PatternsRequestCondition") public interface PatternsRequestCondition { @Nullable Set<String> getPatterns(); } @Pointcut(className = "javax.servlet.Servlet", subTypeRestriction = "org.springframework.web.servlet.DispatcherServlet", methodName = "service", methodParameterTypes = {"javax.servlet.ServletRequest", "javax.servlet.ServletResponse"}) public static class CaptureServletPathAdvice { @OnBefore public static @Nullable FastThreadLocal.Holder</*@Nullable*/ String> onBefore( @BindParameter @Nullable Object req) { if (req == null || !(req instanceof HttpServletRequest)) { return null; } HttpServletRequest request = (HttpServletRequest) req; String contextPath = request.getContextPath(); if (contextPath == null) { contextPath = ""; } String pathInfo = request.getPathInfo(); String servletPath; if (pathInfo == null) { // pathInfo is null when the dispatcher servlet is mapped to "/" (not "/*") and // therefore it is replacing the default servlet and getServletPath() returns the // full path servletPath = contextPath; } else { servletPath = contextPath + request.getServletPath(); } FastThreadLocal.Holder</*@Nullable*/ String> holder = ControllerAspect.servletPath.getHolder(); holder.set(servletPath); return holder; } @OnAfter public static void onAfter( @BindTraveler @Nullable FastThreadLocal.Holder</*@Nullable*/ String> holder) { if (holder != null) { holder.set(null); } } } @Pointcut(className = "org.springframework.web.servlet.handler.AbstractHandlerMethodMapping", methodName = "handleMatch", methodParameterTypes = {"java.lang.Object", "java.lang.String", "javax.servlet.http.HttpServletRequest"}) public static class HandlerMethodMappingAdvice { @OnBefore public static void onBefore(ThreadContext context, @BindParameter @Nullable Object mapping) { if (useAltTransactionNaming.value()) { return; } if (!(mapping instanceof RequestMappingInfo)) { return; } PatternsRequestCondition patternCondition = ((RequestMappingInfo) mapping).glowroot$getPatternsCondition(); if (patternCondition == null) { return; } Set<String> patterns = patternCondition.getPatterns(); if (patterns == null || patterns.isEmpty()) { return; } String prefix = servletPath.get(); String pattern = patterns.iterator().next(); if (pattern == null || pattern.isEmpty()) { context.setTransactionName(prefix, Priority.CORE_PLUGIN); return; } String normalizedPattern = normalizedPatterns.get(pattern); if (normalizedPattern == null) { normalizedPattern = pattern.replaceAll("\\{[^}]*\\}", "*"); normalizedPatterns.put(pattern, normalizedPattern); } if (prefix == null || prefix.isEmpty()) { context.setTransactionName(normalizedPattern, Priority.CORE_PLUGIN); } else { context.setTransactionName(prefix + normalizedPattern, Priority.CORE_PLUGIN); } } } @Pointcut(className = "org.springframework.web.servlet.handler.AbstractUrlHandlerMapping", methodName = "exposePathWithinMapping", methodParameterTypes = {"java.lang.String", "java.lang.String", "javax.servlet.http.HttpServletRequest"}) public static class UrlHandlerMappingAdvice { @OnBefore public static void onBefore(ThreadContext context, @BindParameter @Nullable String bestMatchingPattern) { if (useAltTransactionNaming.value()) { return; } String prefix = servletPath.get(); if (bestMatchingPattern == null || bestMatchingPattern.isEmpty()) { context.setTransactionName(prefix, Priority.CORE_PLUGIN); return; } String normalizedPattern = normalizedPatterns.get(bestMatchingPattern); if (normalizedPattern == null) { normalizedPattern = bestMatchingPattern.replaceAll("\\{[^}]*\\}", "*"); normalizedPatterns.put(bestMatchingPattern, normalizedPattern); } if (prefix == null || prefix.isEmpty()) { context.setTransactionName(normalizedPattern, Priority.CORE_PLUGIN); } else { context.setTransactionName(prefix + normalizedPattern, Priority.CORE_PLUGIN); } } } @Pointcut(classAnnotation = "org.springframework.stereotype.Controller" + "|org.springframework.web.bind.annotation.RestController", methodAnnotation = "org.springframework.web.bind.annotation.RequestMapping", methodParameterTypes = {".."}, timerName = "spring controller") public static class ControllerAdvice { private static final TimerName timerName = Agent.getTimerName(ControllerAdvice.class); @OnBefore public static TraceEntry onBefore(ThreadContext context, @BindMethodMeta ControllerMethodMeta controllerMethodMeta) { if (useAltTransactionNaming.value()) { context.setTransactionName(controllerMethodMeta.getAltTransactionName(), Priority.CORE_PLUGIN); } return context.startTraceEntry(MessageSupplier.create("spring controller: {}.{}()", controllerMethodMeta.getControllerClassName(), controllerMethodMeta.getMethodName()), timerName); } @OnReturn public static void onReturn(@BindTraveler TraceEntry traceEntry) { traceEntry.end(); } @OnThrow public static void onThrow(@BindThrowable Throwable t, @BindTraveler TraceEntry traceEntry) { traceEntry.endWithError(t); } } }