1 /*
2  * Copyright 2008-2019 by Emeric Vernat
3  *
4  *     This file is part of Java Melody.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */

18 package net.bull.javamelody;
19
20 import java.io.IOException;
21 import java.lang.annotation.Annotation;
22 import java.lang.reflect.Method;
23 import java.util.Arrays;
24 import java.util.EventListener;
25 import java.util.HashSet;
26 import java.util.Map;
27 import java.util.Map.Entry;
28
29 import javax.servlet.DispatcherType;
30 import javax.servlet.FilterRegistration;
31 import javax.servlet.ServletContext;
32 import javax.servlet.http.HttpServletRequest;
33 import javax.servlet.http.HttpServletResponse;
34 import javax.sql.DataSource;
35
36 import org.aopalliance.intercept.MethodInvocation;
37 import org.springframework.aop.Pointcut;
38 import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
39 import org.springframework.aop.support.Pointcuts;
40 import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
41 import org.springframework.beans.factory.annotation.Value;
42 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
43 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
44 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
45 import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
46 import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer;
47 import org.springframework.boot.context.properties.EnableConfigurationProperties;
48 import org.springframework.boot.web.servlet.FilterRegistrationBean;
49 import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
50 import org.springframework.context.annotation.Bean;
51 import org.springframework.context.annotation.Configuration;
52 import org.springframework.scheduling.annotation.Async;
53 import org.springframework.scheduling.annotation.Scheduled;
54 import org.springframework.scheduling.annotation.Schedules;
55 import org.springframework.scheduling.quartz.SchedulerFactoryBean;
56 import org.springframework.stereotype.Controller;
57 import org.springframework.stereotype.Service;
58 import org.springframework.web.bind.annotation.RequestMapping;
59 import org.springframework.web.bind.annotation.RestController;
60 import org.springframework.web.client.RestTemplate;
61
62 /**
63  * Spring Boot auto-configuration for JavaMelody.
64  *
65  * <p>
66  * This class is picked up by the Spring Boot auto-configuration mechanism and creates the beans required to set up JavaMelody.
67  * Configuration values are injected using {@link JavaMelodyConfigurationProperties}.
68  * </p>
69  *
70  * <p>
71  * The auto-configured filter can be overridden by defining a custom {@link FilterRegistrationBean} with the name
72  * "javamelody-registration" in the application context.
73  * </p>
74  *
75  * <p>
76  * The configuration is enabled for web applications by default. It is possible to opt-out of the auto-configuration by
77  * setting the application configuration "javamelody.enabled" to the value "false".
78  * </p>
79  *
80  * @author Georg Wittberger, Emeric Vernat
81  * @since 1.64.0
82  */

83 @Configuration
84 @EnableConfigurationProperties(JavaMelodyConfigurationProperties.class)
85 @ConditionalOnWebApplication
86 @ConditionalOnProperty(prefix = JavaMelodyConfigurationProperties.PREFIX, name = "enabled", matchIfMissing = true)
87 public class JavaMelodyAutoConfiguration {
88     /**
89      * Name of the FilterRegistrationBean.
90      */

91     public static final String REGISTRATION_BEAN_NAME = "javamelody-registration";
92
93     private final MonitoredWithAnnotationPointcut monitoredWithSpringAnnotationPointcut = new MonitoredWithAnnotationPointcut();
94
95     private final Pointcut asyncAnnotationPointcut = Pointcuts.union(
96             new AnnotationMatchingPointcut(Async.class),
97             new AnnotationMatchingPointcut(null, Async.class));
98
99     private final Pointcut scheduledAnnotationPointcut = Pointcuts.union(
100             new AnnotationMatchingPointcut(null, Scheduled.class),
101             new AnnotationMatchingPointcut(null, Schedules.class));
102
103     /**
104      * Registers the JavaMelody {@link SessionListener}.
105      * @param servletContext ServletContext
106      * @return ServletListenerRegistrationBean
107      */

108     @Bean
109     public ServletListenerRegistrationBean<EventListener> monitoringSessionListener(
110             ServletContext servletContext) {
111         final ServletListenerRegistrationBean<EventListener> servletListenerRegistrationBean = new ServletListenerRegistrationBean<>(
112                 new SessionListener());
113         if (servletContext.getFilterRegistration("javamelody") != null) {
114             // if webapp deployed as war in a container with MonitoringFilter and SessionListener already added by web-fragment.xml,
115             // do not add again
116             servletListenerRegistrationBean.setEnabled(false);
117         }
118         return servletListenerRegistrationBean;
119     }
120
121     /**
122      * Registers the JavaMelody {@link MonitoringFilter}. The filter can be overridden completely by creating a custom
123      * {@link FilterRegistrationBean} with the name "javamelody-registration" in the application context.
124      * @param properties JavaMelodyConfigurationProperties
125      * @param servletContext ServletContext
126      * @return FilterRegistrationBean
127      */

128     @Bean(name = REGISTRATION_BEAN_NAME)
129     @ConditionalOnMissingBean(name = REGISTRATION_BEAN_NAME)
130     public FilterRegistrationBean<MonitoringFilter> monitoringFilter(
131             JavaMelodyConfigurationProperties properties, ServletContext servletContext) {
132         final FilterRegistrationBean<MonitoringFilter> registrationBean = new FilterRegistrationBean<>();
133
134         // Create the monitoring filter and set its configuration parameters.
135         final MonitoringFilter filter;
136         if (properties.isManagementEndpointMonitoringEnabled()) {
137             // if the management endpoint is enabled, disable the /monitoring reports on the application port
138             filter = new MonitoringFilter() {
139                 @Override
140                 protected boolean isAllowed(HttpServletRequest request,
141                         HttpServletResponse response) throws IOException {
142                     response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden access");
143                     return false;
144                 }
145             };
146         } else {
147             filter = new MonitoringFilter();
148         }
149         filter.setApplicationType("Spring Boot");
150
151         // Wrap the monitoring filter in the registration bean.
152         registrationBean.setFilter(filter);
153         registrationBean.setAsyncSupported(true);
154         registrationBean.setName("javamelody");
155         registrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
156
157         // Set the initialization parameter for the monitoring filter.
158         for (final Entry<String, String> parameter : properties.getInitParameters().entrySet()) {
159             registrationBean.addInitParameter(parameter.getKey(), parameter.getValue());
160         }
161
162         // Set the URL patterns to activate the monitoring filter for.
163         registrationBean.addUrlPatterns("/*");
164
165         final FilterRegistration filterRegistration = servletContext
166                 .getFilterRegistration("javamelody");
167         if (filterRegistration != null) {
168             // if webapp deployed as war in a container with MonitoringFilter already added by web-fragment.xml,
169             // do not try to add it again
170             registrationBean.setEnabled(false);
171             for (final Map.Entry<String, String> entry : registrationBean.getInitParameters()
172                     .entrySet()) {
173                 filterRegistration.setInitParameter(entry.getKey(), entry.getValue());
174             }
175         }
176         return registrationBean;
177     }
178
179     /**
180      * When enabled, management endpoint for /monitoring reports on the management http port instead of the application http port.
181      * @param servletContext ServletContext
182      * @return MonitoringEndpoint
183      */

184     @Bean
185     @ConditionalOnProperty(prefix = JavaMelodyConfigurationProperties.PREFIX, name = "management-endpoint-monitoring-enabled", matchIfMissing = false)
186     public MonitoringEndpoint monitoringEndpoint(final ServletContext servletContext) {
187         return new MonitoringEndpoint(servletContext);
188     }
189
190     /**
191      * Now disabled by default, since dependency spring-boot-starter-aop was added in 1.76.
192      * @return DefaultAdvisorAutoProxyCreator
193      */

194     @Bean
195     @ConditionalOnMissingBean(DefaultAdvisorAutoProxyCreator.class)
196     @ConditionalOnProperty(prefix = JavaMelodyConfigurationProperties.PREFIX, name = "advisor-auto-proxy-creator-enabled", matchIfMissing = false)
197     public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
198         return new DefaultAdvisorAutoProxyCreator();
199     }
200
201     /**
202      * Monitoring of JDBC {@link DataSource}s
203      * @param excludedDatasources Comma separated list of excluded datasources
204      * @return SpringDataSourceBeanPostProcessor
205      */

206     @Bean
207     public SpringDataSourceBeanPostProcessor monitoringDataSourceBeanPostProcessor(
208             @Value("${javamelody.excluded-datasources:}") String excludedDatasources) {
209         // IMPORTANT: We cannot inject JavaMelodyConfigurationProperties here because of bean load order! Therefore we have
210         // to use that rather dirty way to inject the configuration value.
211         final SpringDataSourceBeanPostProcessor processor = new SpringDataSourceBeanPostProcessor();
212         if (excludedDatasources != null && excludedDatasources.trim().length() > 0) {
213             processor.setExcludedDatasources(
214                     new HashSet<>(Arrays.asList(excludedDatasources.split(","))));
215         }
216         return processor;
217     }
218
219     /**
220      * Monitoring of beans and methods having the {@link MonitoredWithSpring} annotation.
221      * @return MonitoringSpringAdvisor
222      */

223     @Bean
224     @ConditionalOnProperty(prefix = JavaMelodyConfigurationProperties.PREFIX, name = "spring-monitoring-enabled", matchIfMissing = true)
225     public MonitoringSpringAdvisor monitoringSpringAdvisor() {
226         return new MonitoringSpringAdvisor(monitoredWithSpringAnnotationPointcut);
227     }
228
229     /**
230      * Monitoring of beans having the {@link Service} annotation.
231      * @return MonitoringSpringAdvisor
232      */

233     @Bean
234     @ConditionalOnProperty(prefix = JavaMelodyConfigurationProperties.PREFIX, name = "spring-monitoring-enabled", matchIfMissing = true)
235     public MonitoringSpringAdvisor monitoringSpringServiceAdvisor() {
236         return createMonitoringSpringAdvisorWithExclusions(
237                 new AnnotationMatchingPointcut(Service.class),
238                 monitoredWithSpringAnnotationPointcut, asyncAnnotationPointcut,
239                 scheduledAnnotationPointcut);
240     }
241
242     /**
243      * Monitoring of beans having the {@link Controller} annotation.
244      * @return MonitoringSpringAdvisor
245      */

246     @Bean
247     @ConditionalOnProperty(prefix = JavaMelodyConfigurationProperties.PREFIX, name = "spring-monitoring-enabled", matchIfMissing = true)
248     public MonitoringSpringAdvisor monitoringSpringControllerAdvisor() {
249         return createMonitoringSpringAdvisorWithExclusions(
250                 new AnnotationMatchingPointcut(Controller.class),
251                 monitoredWithSpringAnnotationPointcut, asyncAnnotationPointcut,
252                 scheduledAnnotationPointcut);
253     }
254
255     /**
256      * Monitoring of beans having the {@link RestController} annotation.
257      * @return MonitoringSpringAdvisor
258      */

259     @Bean
260     @ConditionalOnProperty(prefix = JavaMelodyConfigurationProperties.PREFIX, name = "spring-monitoring-enabled", matchIfMissing = true)
261     public MonitoringSpringAdvisor monitoringSpringRestControllerAdvisor() {
262         return createMonitoringSpringAdvisorWithExclusions(
263                 new AnnotationMatchingPointcut(RestController.class),
264                 monitoredWithSpringAnnotationPointcut, asyncAnnotationPointcut,
265                 scheduledAnnotationPointcut);
266     }
267
268     /**
269      * Monitoring of beans or methods having the {@link Async} annotation.
270      * @return MonitoringSpringAdvisor
271      */

272     @Bean
273     @ConditionalOnProperty(prefix = JavaMelodyConfigurationProperties.PREFIX, name = "spring-monitoring-enabled", matchIfMissing = true)
274     public MonitoringSpringAdvisor monitoringSpringAsyncAdvisor() {
275         return createMonitoringSpringAdvisorWithExclusions(asyncAnnotationPointcut,
276                 monitoredWithSpringAnnotationPointcut, scheduledAnnotationPointcut);
277     }
278
279     /**
280      * Monitoring of beans methods having the {@link Scheduled} or {@link Schedules} annotations.
281      * @return MonitoringSpringAdvisor
282      */

283     @Bean
284     @ConditionalOnProperty(prefix = JavaMelodyConfigurationProperties.PREFIX, name = "scheduled-monitoring-enabled", matchIfMissing = true)
285     @ConditionalOnMissingBean(DefaultAdvisorAutoProxyCreator.class)
286     public MonitoringSpringAdvisor monitoringSpringScheduledAdvisor() {
287         return createMonitoringSpringAdvisorWithExclusions(scheduledAnnotationPointcut,
288                 monitoredWithSpringAnnotationPointcut, asyncAnnotationPointcut);
289     }
290
291     private MonitoringSpringAdvisor createMonitoringSpringAdvisorWithExclusions(Pointcut pointcut,
292             Pointcut... excludedPointcuts) {
293         final Pointcut myPointcut;
294         if (excludedPointcuts.length == 0) {
295             myPointcut = pointcut;
296         } else {
297             Pointcut excludedPointcut = excludedPointcuts[0];
298             if (excludedPointcuts.length > 1) {
299                 for (int i = 1; i < excludedPointcuts.length; i++) {
300                     excludedPointcut = Pointcuts.union(excludedPointcut, excludedPointcuts[i]);
301                 }
302             }
303             myPointcut = new ExcludingPointcut(pointcut).exclude(excludedPointcut);
304         }
305         return new MonitoringSpringAdvisor(myPointcut);
306     }
307
308     /**
309      * Monitoring of Feign clients.
310      * @return MonitoringSpringAdvisor
311      * @throws ClassNotFoundException should not happen
312      */

313     @SuppressWarnings("unchecked")
314     @Bean
315     @ConditionalOnClass(name = "org.springframework.cloud.openfeign.FeignClient")
316     @ConditionalOnProperty(prefix = JavaMelodyConfigurationProperties.PREFIX, name = "spring-monitoring-enabled", matchIfMissing = true)
317     // we check that the DefaultAdvisorAutoProxyCreator above is not enabled, because if it's enabled then feign calls are counted twice
318     @ConditionalOnMissingBean(DefaultAdvisorAutoProxyCreator.class)
319     public MonitoringSpringAdvisor monitoringFeignClientAdvisor() throws ClassNotFoundException {
320         final Class<? extends Annotation> feignClientClass = (Class<? extends Annotation>) Class
321                 .forName("org.springframework.cloud.openfeign.FeignClient");
322         final MonitoringSpringAdvisor advisor = new MonitoringSpringAdvisor(
323                 new AnnotationMatchingPointcut(feignClientClass, true));
324         advisor.setAdvice(new MonitoringSpringInterceptor() {
325             private static final long serialVersionUID = 1L;
326
327             @Override
328             protected String getRequestName(MethodInvocation invocation) {
329                 final StringBuilder sb = new StringBuilder();
330                 final Method method = invocation.getMethod();
331                 final RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
332                 if (requestMapping != null) {
333                     String[] path = requestMapping.value();
334                     if (path.length == 0) {
335                         path = requestMapping.path();
336                     }
337                     if (path.length > 0) {
338                         sb.append(path[0]);
339                         sb.append(' ');
340                         if (requestMapping.method().length > 0) {
341                             sb.append(requestMapping.method()[0].name());
342                         } else {
343                             sb.append("GET");
344                         }
345                         sb.append('\n');
346                     }
347                 }
348                 final Class<?> declaringClass = method.getDeclaringClass();
349                 final String classPart = declaringClass.getSimpleName();
350                 final String methodPart = method.getName();
351                 sb.append(classPart).append('.').append(methodPart);
352                 return sb.toString();
353             }
354         });
355         return advisor;
356     }
357
358     /**
359      * Monitoring of {@link RestTemplate} beans.
360      * @return SpringRestTemplateBeanPostProcessor
361      */

362     @Bean
363     @ConditionalOnProperty(prefix = JavaMelodyConfigurationProperties.PREFIX, name = "spring-monitoring-enabled", matchIfMissing = true)
364     public SpringRestTemplateBeanPostProcessor monitoringRestTemplateBeanPostProcessor() {
365         return new SpringRestTemplateBeanPostProcessor();
366     }
367
368     /**
369      * Monitoring of MongoDbFactory beans.
370      * @return SpringMongoDbFactoryBeanPostProcessor
371      */

372     @Bean
373     @ConditionalOnProperty(prefix = JavaMelodyConfigurationProperties.PREFIX, name = "spring-monitoring-enabled", matchIfMissing = true)
374     public SpringMongoDbFactoryBeanPostProcessor monitoringMongoDbFactoryBeanPostProcessor() {
375         return new SpringMongoDbFactoryBeanPostProcessor();
376     }
377
378     /**
379      * Monitoring of ElasticsearchOperations beans.
380      * @return SpringElasticsearchOperationsBeanPostProcessor
381      */

382     @Bean
383     @ConditionalOnProperty(prefix = JavaMelodyConfigurationProperties.PREFIX, name = "spring-monitoring-enabled", matchIfMissing = true)
384     public SpringElasticsearchOperationsBeanPostProcessor monitoringElasticsearchOperationsBeanPostProcessor() {
385         return new SpringElasticsearchOperationsBeanPostProcessor();
386     }
387
388     /**
389      * Configure Spring's Schedulers for Quartz Scheduler
390      * @return SchedulerFactoryBeanCustomizer
391      */

392     @ConditionalOnClass(name = {
393             "org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer",
394             "org.springframework.scheduling.quartz.SchedulerFactoryBean",
395             "org.quartz.JobListener" })
396     @Bean
397     @ConditionalOnMissingBean
398     public SchedulerFactoryBeanCustomizer schedulerFactoryBeanCustomizer() {
399         return new SchedulerFactoryBeanCustomizer() {
400             @Override
401             public void customize(SchedulerFactoryBean schedulerFactoryBean) {
402                 final JobGlobalListener jobGlobalListener = new JobGlobalListener();
403                 schedulerFactoryBean.setGlobalJobListeners(jobGlobalListener);
404                 schedulerFactoryBean.setExposeSchedulerInRepository(true);
405             }
406         };
407     }
408
409     /**
410      * @return Enregistrement du context Spring.
411      */

412     @Bean
413     public SpringContext javamelodySpringContext() {
414         return new SpringContext();
415     }
416 }
417