├── .gitignore
├── README.md
├── pom.xml
├── scripts
└── aggregate_metrics.py
├── src
├── main
│ ├── java
│ │ └── com
│ │ │ └── willvuong
│ │ │ └── bootstrapper
│ │ │ ├── config
│ │ │ ├── AppConfiguration.java
│ │ │ └── MetricsConfiguration.java
│ │ │ ├── controllers
│ │ │ ├── ErrorHandlerController.java
│ │ │ └── HomeController.java
│ │ │ ├── filter
│ │ │ ├── LogbackResponseServletFilter.java
│ │ │ └── RequestMDCServletFilter.java
│ │ │ ├── mvcconfig
│ │ │ ├── GlobalDefaultExceptionHandler.java
│ │ │ ├── MvcConfiguration.java
│ │ │ └── ServletContextAutoConfigure.java
│ │ │ └── util
│ │ │ └── ThreadLocalMemoryAppender.java
│ ├── resources
│ │ └── logback.xml
│ └── webapp
│ │ ├── WEB-INF
│ │ ├── decorators.xml
│ │ ├── decorators
│ │ │ └── decorator.jsp
│ │ ├── views
│ │ │ ├── angular-index.jsp
│ │ │ ├── error.jsp
│ │ │ └── index-async.html
│ │ └── web.xml
│ │ └── resources
│ │ ├── css
│ │ ├── .gitignore
│ │ └── app.css
│ │ ├── img
│ │ └── .gitignore
│ │ ├── index-async.html
│ │ ├── index.html
│ │ ├── js
│ │ ├── app.js
│ │ ├── controllers.js
│ │ ├── directives.js
│ │ ├── filters.js
│ │ └── services.js
│ │ ├── lib
│ │ ├── angular
│ │ │ ├── angular-cookies.js
│ │ │ ├── angular-cookies.min.js
│ │ │ ├── angular-loader.js
│ │ │ ├── angular-loader.min.js
│ │ │ ├── angular-resource.js
│ │ │ ├── angular-resource.min.js
│ │ │ ├── angular-sanitize.js
│ │ │ ├── angular-sanitize.min.js
│ │ │ ├── angular.js
│ │ │ ├── angular.min.js
│ │ │ └── version.txt
│ │ └── bootstrap
│ │ │ ├── css
│ │ │ ├── bootstrap-theme.css
│ │ │ ├── bootstrap-theme.min.css
│ │ │ ├── bootstrap.css
│ │ │ └── bootstrap.min.css
│ │ │ ├── fonts
│ │ │ ├── glyphicons-halflings-regular.eot
│ │ │ ├── glyphicons-halflings-regular.svg
│ │ │ ├── glyphicons-halflings-regular.ttf
│ │ │ └── glyphicons-halflings-regular.woff
│ │ │ └── js
│ │ │ ├── bootstrap.js
│ │ │ └── bootstrap.min.js
│ │ └── partials
│ │ ├── .gitignore
│ │ ├── partial1.html
│ │ └── partial2.html
└── test
│ └── java
│ └── com
│ └── willvuong
│ └── bootstrapper
│ └── util
│ └── ThreadLocalMemoryAppenderTest.java
└── tools
└── springloaded-1.1.4.jar
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | .idea
3 | *.iml
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | spring-mvc-bootstrap-angularjs-starter
2 | ======================================
3 |
4 | The purpose of this is to serve as a project base that has the following things built in:
5 | * Maven project structure and configuration
6 | * Spring MVC and default mappings
7 | * Bootstrap resources and starter page
8 | * AngularJS resources and starter page
9 | * Application defaults like logging, exception handling, and login.
10 |
11 | So clone it and get started.
12 |
13 | Main components
14 | ----------------------
15 | * Build and tooling via [Maven 3.1+](http://maven.apache.org)
16 | * Servlet 3.x and [Spring Framework 3.2.x](http://docs.spring.io/spring/docs/3.2.5.RELEASE/spring-framework-reference/htmlsingle/)
17 | * [SiteMesh 2.4](http://wiki.sitemesh.org/display/sitemesh/Home) to decorate layouts
18 | * [Bootstrap 3.x](http://getbootstrap.com/) styling and [AngularJS](http://angularjs.org/)
19 | * Logging via [SLF4J](http://www.slf4j.org/) and [Logback](http://logback.qos.ch/)
20 | * Devopsy monitoring things via [Metrics](http://metrics.codahale.com/)
21 |
22 | How it is configured
23 | ----------------------
24 | Maven is configured to compile for JDK7 so make sure that you have your `JAVA_HOME` configured correctly (you can check this also by running `mvn --version`).
25 |
26 | Let's get this thing running:
27 |
28 | ```
29 | mvn jetty:run
30 | ```
31 |
32 | The root of all configuration starts in the `src/main/webapp/WEB-INF/web.xml` file. The web.xml uses the `ContextLoaderListener` to initialize the Spring application context via `@Configuration` annotation scanning (the `contextClass` and `contextConfigLocation` context-params). In this case, the web.xml's `contextConfigLocation` context-param directs the `ContextLoaderListener` to load the main/parent application context in the annotated class `com.willvuong.bootstrapper.config.AppConfiguration`.
33 |
34 | ``` xml
35 |
36 | contextConfigLocation
37 | com.willvuong.bootstrapper.config.AppConfiguration
38 |
39 | ```
40 |
41 | The `AppConfiguration` class should use `@Import` and/or `@ComponentScan` annotations to import other `@Configuration` classes or annotated `@Component`-ish classes.
42 |
43 | The Spring MVC context is configured via the `DispatcherServlet`'s `contextConfigLocation` init-param value:
44 |
45 | ``` xml
46 |
47 | appServlet
48 | org.springframework.web.servlet.DispatcherServlet
49 |
50 | contextClass
51 | org.springframework.web.context.support.AnnotationConfigWebApplicationContext
52 |
53 |
54 | contextConfigLocation
55 | com.willvuong.bootstrapper.mvcconfig.MvcConfiguration
56 |
57 | 1
58 | true
59 |
60 | ```
61 |
62 | The `MvcConfiguration` class configures the Spring MVC framework with some "classic" defaults and already has a `@ComponentScan` annotation for mapping annotated `@Controller` classes in the `com.willvuong.bootstrapper.controllers` package.
63 |
64 | Let's talk about the "classic" defaults:
65 | * The `DispatcherServlet` is configured to map to all requests within the servlet context via `/*`.
66 | * `MvcConfiguration` is configured to route all `/resources/**` requests to static resources located in `src/main/webapp/resources`.
67 | * All unmatched requests are finally routed to the [default servlet handler](http://docs.spring.io/spring/docs/3.2.x/javadoc-api/org/springframework/web/servlet/config/annotation/WebMvcConfigurerAdapter.html#configureDefaultServletHandling(org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer)).
68 |
69 | If you paid attention to the console output during `mvn jetty:run`, you might have seen a few lines that begin with "autoconfiguring blah blah...". These log statements originate from the class `com.willvuong.bootstrapper.mvcconfig.ServletContextAutoConfigure` which does some additional configuration of the servlet context.
70 | * A utility filter `com.willvuong.bootstrapper.filter.RequestMDCServletFilter` is added to the servlet context. Its main purpose is to set details from the current `HttpServletRequest` (request URL, request URI, and generate a unique logging ID for the scope of the current request) into the SLF4J MDC.
71 | * Configure SiteMesh to decorate all responses except for static resources in `/resources/**`. SiteMesh is configured by `src/main/webapp/WEB-INF/decorators.xml`.
72 | * Configure Metrics instrumentation and reporting via Metrics servlets.
73 |
74 | Another thing to pay attention to is the Logback configuration (via `src/main/resources/logback.xml`).
75 | * Logback is started in debug mode and will refresh its configuration every 30 seconds.
76 | * It will log to the console in color for readability. (If you are running in a terminal emulator without color, you can disable colors like `mvn jetty:run -Dnocolor=true`.)
77 | * Log messages are very detailed and include the current request URI and request ID. This helps to map request handlers to log messages.
78 |
79 | Open up a web browser and navigate to `localhost:8080` and click on the currently running servlet context (if you didn't change the pom.xml artifactId yet, it will be "spring-mvc-bootstrap-angularjs-starter").
80 | * The web page you should see is mapped by controller `com.willvuong.bootstrapper.controllers.HomeController` which all it does is forward to `/WEB-INF/views/angular-index.jsp`.
81 | * JSP `/WEB-INF/views/angular-index.jsp` is located at `src/main/webapp/WEB-INF/views/angular-index.jsp`. It references JavaScript and Angular resources in `/resources/` (`src/main/webapp/resources`).
82 | * The response is decorated by SiteMesh by `src/main/webapp/WEB-INF/decorators/default.jsp`. This JSP decorator wraps angular-index.jsp in Bootstrap CSS styling in `/resources/` (`src/main/webapp/resources`).
83 |
84 | Metrics reporting is exposed via:
85 | * Application metrics: {contextpath}/diagnostics/metrics
86 | * JVM metrics: {contextpath}/diagnostics/jvm
87 | * Thread dump: {contextpath}/diagnostics/threads
88 | * Health checks: {contextpath}/diagnostics/health
89 | * Ping: {contextpath}/diagnostics/ping
90 |
91 | To kill the Jetty server, use `Control-c`.
92 |
93 | Hot development mode (aka class reloading)
94 | ----------------------
95 | You could just reload the servlet context in the Jetty command line after every recompile by hitting enter (at least until you run out of heap space!) but that really sucks most times. (Also a reminder that you don't have to reload the servlet context for JSP or static resource changes as they are LIVE since `mvn jetty:run` is serving directly out of `src/main/webapp`.)
96 |
97 | If you have [JRebel](http://zeroturnaround.com/software/jrebel/), you should already be good to go via IDE plugin or [attaching the JRebel agent to the JVM](http://manuals.zeroturnaround.com/jrebel/standalone/launch-quick-start.html).
98 |
99 | Otherwise, the [Spring Loaded](https://github.com/spring-projects/spring-loaded) project can serve as a poor man's JRebel in a pinch. The Spring Loaded jar is located in the `tools` directory and all of the configuration is in the pom.xml (jetty-maven-plugin and the debug profile).
100 |
101 | To start your app in Jetty with Spring Loaded:
102 | `mvn jetty:run-forked`
103 |
104 | To start your app in Jetty with Spring Loaded and JDWP on port `5005`:
105 | `mvn jetty:run-forked -P debug`
106 |
107 | Packaging for deployment
108 | ----------------------
109 | TODO
110 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | 4.0.0
6 | com.willvuong
7 | spring-mvc-bootstrap-angularjs-starter
8 | spring-mvc-bootstrap-angularjs-starter
9 | war
10 | 1.0.0-BUILD-SNAPSHOT
11 |
12 |
13 | 1.7
14 | 3.2.5.RELEASE
15 | 1.7.4
16 | 1.7.5
17 | 3.0.1
18 |
19 |
20 |
21 |
22 |
23 | com.google.guava
24 | guava
25 | 15.0
26 |
27 |
28 |
29 | org.apache.commons
30 | commons-lang3
31 | 3.1
32 |
33 |
34 |
35 |
36 | org.springframework
37 | spring-context
38 | ${org.springframework-version}
39 |
40 |
41 |
42 | commons-logging
43 | commons-logging
44 |
45 |
46 |
47 |
48 |
49 | org.springframework
50 | spring-webmvc
51 | ${org.springframework-version}
52 |
53 |
54 |
55 |
56 | org.aspectj
57 | aspectjrt
58 | ${org.aspectj-version}
59 |
60 |
61 |
62 |
63 | org.slf4j
64 | slf4j-api
65 | ${org.slf4j-version}
66 |
67 |
68 |
69 | org.slf4j
70 | jcl-over-slf4j
71 | ${org.slf4j-version}
72 | runtime
73 |
74 |
75 |
76 | ch.qos.logback
77 | logback-classic
78 | 1.0.13
79 |
80 |
81 |
82 | org.codehaus.janino
83 | janino
84 | 2.6.1
85 |
86 |
87 |
88 | org.fusesource.jansi
89 | jansi
90 | 1.11
91 |
92 |
93 |
94 |
95 | javax.inject
96 | javax.inject
97 | 1
98 |
99 |
100 |
101 |
102 | javax.servlet
103 | javax.servlet-api
104 | 3.1.0
105 | provided
106 |
107 |
108 |
109 | javax.servlet.jsp
110 | jsp-api
111 | 2.1
112 | provided
113 |
114 |
115 |
116 | javax.servlet.jsp.jstl
117 | jstl-api
118 | 1.2
119 |
120 |
121 | javax.servlet
122 | servlet-api
123 |
124 |
125 |
126 |
127 |
128 | org.glassfish.web
129 | jstl-impl
130 | 1.2
131 |
132 |
133 | javax.servlet
134 | servlet-api
135 |
136 |
137 |
138 |
139 |
140 | opensymphony
141 | sitemesh
142 | 2.4.2
143 |
144 |
145 |
146 |
147 | org.codehaus.jackson
148 | jackson-mapper-asl
149 | 1.8.1
150 |
151 |
152 |
153 |
154 | javax.validation
155 | validation-api
156 | 1.0.0.GA
157 |
158 |
159 |
160 | org.hibernate
161 | hibernate-validator
162 | 4.1.0.Final
163 |
164 |
165 |
166 |
167 | joda-time
168 | joda-time
169 | 1.6.2
170 |
171 |
172 |
173 |
174 | com.codahale.metrics
175 | metrics-core
176 | ${metrics-version}
177 |
178 |
179 |
180 | com.codahale.metrics
181 | metrics-jvm
182 | ${metrics-version}
183 |
184 |
185 |
186 | com.codahale.metrics
187 | metrics-servlets
188 | ${metrics-version}
189 |
190 |
191 |
192 | com.codahale.metrics
193 | metrics-servlet
194 | ${metrics-version}
195 |
196 |
197 |
198 |
199 | org.springframework
200 | spring-test
201 | ${org.springframework-version}
202 | test
203 |
204 |
205 |
206 | junit
207 | junit
208 | 4.8.2
209 | test
210 |
211 |
212 |
213 | xmlunit
214 | xmlunit
215 | 1.2
216 | test
217 |
218 |
219 |
220 | com.jayway.jsonpath
221 | json-path
222 | 0.8.1
223 | test
224 |
225 |
226 |
227 | org.hamcrest
228 | hamcrest-library
229 | 1.3
230 | test
231 |
232 |
233 |
234 |
235 |
236 |
237 | org.springframework.maven.snapshot
238 | Spring Maven Snapshot Repository
239 | http://maven.springframework.org/snapshot
240 |
241 | false
242 |
243 |
244 | true
245 |
246 |
247 |
248 |
249 |
250 | org.springframework.maven.milestone
251 | Spring Maven Milestone Repository
252 | http://maven.springframework.org/milestone
253 |
254 | false
255 |
256 |
257 |
258 |
259 |
260 |
261 | debug
262 |
263 | -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 | org.apache.maven.plugins
272 | maven-compiler-plugin
273 | 3.1
274 |
275 | ${java-version}
276 | ${java-version}
277 |
278 |
279 |
280 |
281 | org.apache.maven.plugins
282 | maven-surefire-plugin
283 | 2.16
284 |
285 |
286 | **/*Tests.java
287 |
288 |
289 | **/Abstract*.java
290 |
291 | junit:junit
292 | -Xmx512m
293 |
294 |
295 |
296 |
297 | org.apache.maven.plugins
298 | maven-dependency-plugin
299 | 2.8
300 |
301 |
302 | install
303 | install
304 |
305 | sources
306 |
307 |
308 |
309 |
310 |
311 |
312 | org.apache.tomcat.maven
313 | tomcat7-maven-plugin
314 | 2.0
315 |
316 |
317 |
318 | org.mortbay.jetty
319 | jetty-maven-plugin
320 | 8.1.14.v20131031
321 |
322 |
323 | /${project.artifactId}
324 |
325 | manual
326 | foo
327 | 8082
328 |
329 | true
330 | -javaagent:${basedir}/tools/springloaded-1.1.4.jar -noverify ${jdwp}
331 |
332 |
333 |
334 |
335 |
336 |
--------------------------------------------------------------------------------
/scripts/aggregate_metrics.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import urllib2
3 | import json
4 | from collections import defaultdict
5 | from collections import Counter
6 | import numpy
7 |
8 | def main(argv):
9 | urls = argv[0:]
10 | counters = Counter()
11 | meters = defaultdict(Counter)
12 | timers = defaultdict(Counter)
13 |
14 | for url in urls:
15 | f = urllib2.urlopen(url)
16 | metric = json.loads(f.read())
17 |
18 | # merge counters
19 | for counter in metric['counters'].keys():
20 | # see if counter already exists
21 | # print counter
22 | counters[counter] += metric['counters'][counter]['count']
23 |
24 | # merge meters
25 | for meter in metric['meters'].keys():
26 | # print meter
27 | meters[meter]['count'] += metric['meters'][meter]['count']
28 |
29 | # merge timers
30 | for timer, values in metric['timers'].items():
31 | timers[timer]['count'] += metric['timers'][timer]['count']
32 | timers[timer]['max'] = max(timers[timer]['max'], metric['timers'][timer]['max'])
33 |
34 | if timers[timer]['min'] == 0:
35 | timers[timer]['min'] = metric['timers'][timer]['min']
36 | else:
37 | timers[timer]['min'] = min(timers[timer]['min'], metric['timers'][timer]['min'])
38 |
39 | if timers[timer]['means'] == 0:
40 | timers[timer]['means'] = []
41 | timers[timer]['p50s'] = []
42 | timers[timer]['p75s'] = []
43 | timers[timer]['p95s'] = []
44 | timers[timer]['p98s'] = []
45 | timers[timer]['p99s'] = []
46 | timers[timer]['p999s'] = []
47 | timers[timer]['weight'] = []
48 |
49 | timers[timer]['means'].append(metric['timers'][timer]['mean'])
50 | timers[timer]['p50s'].append(metric['timers'][timer]['p50'])
51 | timers[timer]['p75s'].append(metric['timers'][timer]['p75'])
52 | timers[timer]['p95s'].append(metric['timers'][timer]['p95'])
53 | timers[timer]['p98s'].append(metric['timers'][timer]['p98'])
54 | timers[timer]['p99s'].append(metric['timers'][timer]['p99'])
55 | timers[timer]['p999s'].append(metric['timers'][timer]['p999'])
56 | timers[timer]['weight'].append(metric['timers'][timer]['count'])
57 | timers[timer]['duration_units'] = metric['timers'][timer]['duration_units']
58 |
59 | # average timers
60 | for name, timer in timers.items():
61 | timer['mean'] = numpy.average(timer['means'], weights=timer['weight'])
62 | timer['p50'] = numpy.average(timer['p50s'], weights=timer['weight'])
63 | timer['p75'] = numpy.average(timer['p75s'], weights=timer['weight'])
64 | timer['p95'] = numpy.average(timer['p95s'], weights=timer['weight'])
65 | timer['p98'] = numpy.average(timer['p98s'], weights=timer['weight'])
66 | timer['p99'] = numpy.average(timer['p99s'], weights=timer['weight'])
67 | timer['p999'] = numpy.average(timer['p999s'], weights=timer['weight'])
68 | del timer['means']
69 | del timer['p50s']
70 | del timer['p75s']
71 | del timer['p95s']
72 | del timer['p98s']
73 | del timer['p99s']
74 | del timer['p999s']
75 | del timer['weight']
76 |
77 | print '=' * 80
78 | print 'COUNTERS'
79 | for k, v in counters.items():
80 | print ' {0}: {1}'.format(k, v)
81 | print
82 |
83 | print '=' * 80
84 | print 'METERS'
85 | for key, counter in sorted(meters.items()):
86 | print key
87 | for k, v in counter.items():
88 | print ' {0}: {1}'.format(k, v)
89 | print
90 |
91 | print '=' * 80
92 | print 'TIMERS'
93 | for key, counter in sorted(timers.items()):
94 | print key
95 | for k, v in sorted(counter.items()):
96 | print ' {0}: {1}'.format(k, v)
97 |
98 | if __name__ == "__main__":
99 | main(sys.argv[1:])
100 |
--------------------------------------------------------------------------------
/src/main/java/com/willvuong/bootstrapper/config/AppConfiguration.java:
--------------------------------------------------------------------------------
1 | package com.willvuong.bootstrapper.config;
2 |
3 | import org.springframework.context.annotation.Configuration;
4 | import org.springframework.context.annotation.Import;
5 |
6 | /**
7 | * Created with IntelliJ IDEA.
8 | * User: will
9 | * Date: 11/23/13
10 | * Time: 12:34 PM
11 | */
12 | @Configuration
13 | @Import(MetricsConfiguration.class)
14 | public class AppConfiguration {
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/src/main/java/com/willvuong/bootstrapper/config/MetricsConfiguration.java:
--------------------------------------------------------------------------------
1 | package com.willvuong.bootstrapper.config;
2 |
3 | import com.codahale.metrics.MetricRegistry;
4 | import com.codahale.metrics.health.HealthCheckRegistry;
5 | import org.springframework.context.annotation.Bean;
6 | import org.springframework.context.annotation.Configuration;
7 |
8 | /**
9 | * Created with IntelliJ IDEA.
10 | * User: will
11 | * Date: 12/5/13
12 | * Time: 2:45 PM
13 | */
14 | @Configuration
15 | public class MetricsConfiguration {
16 |
17 | @Bean
18 | public MetricRegistry metricRegistry() {
19 | return new MetricRegistry();
20 | }
21 |
22 | @Bean
23 | public HealthCheckRegistry healthCheckRegistry() {
24 | return new HealthCheckRegistry();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/com/willvuong/bootstrapper/controllers/ErrorHandlerController.java:
--------------------------------------------------------------------------------
1 | package com.willvuong.bootstrapper.controllers;
2 |
3 | import org.springframework.stereotype.Controller;
4 | import org.springframework.web.bind.annotation.RequestMapping;
5 | import org.springframework.web.bind.annotation.RequestParam;
6 | import org.springframework.web.bind.annotation.ResponseBody;
7 |
8 | /**
9 | * Created with IntelliJ IDEA.
10 | * User: will
11 | * Date: 11/23/13
12 | * Time: 3:14 PM
13 | */
14 | @Controller
15 | public class ErrorHandlerController {
16 |
17 | @RequestMapping("/error")
18 | public String errorView() {
19 | return "/WEB-INF/views/error.jsp";
20 | }
21 |
22 | @RequestMapping("/debug/ThrowException")
23 | @ResponseBody
24 | public String throwException(@RequestParam String really) {
25 | if ("true".equals(really)) {
26 | throw new RuntimeException("intentional exception throw to invoke error handler", new RuntimeException("causal exception"));
27 | }
28 |
29 | return "really=" + really;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/com/willvuong/bootstrapper/controllers/HomeController.java:
--------------------------------------------------------------------------------
1 | package com.willvuong.bootstrapper.controllers;
2 |
3 | import org.springframework.stereotype.Controller;
4 | import org.springframework.web.bind.annotation.RequestMapping;
5 |
6 | /**
7 | * Created with IntelliJ IDEA.
8 | * User: will
9 | * Date: 11/23/13
10 | * Time: 12:37 PM
11 | */
12 | @Controller
13 | @RequestMapping("/")
14 | public class HomeController {
15 |
16 | @RequestMapping
17 | public String home() {
18 | return "/WEB-INF/views/angular-index.jsp";
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/com/willvuong/bootstrapper/filter/LogbackResponseServletFilter.java:
--------------------------------------------------------------------------------
1 | package com.willvuong.bootstrapper.filter;
2 |
3 | import ch.qos.logback.classic.LoggerContext;
4 | import com.willvuong.bootstrapper.util.ThreadLocalMemoryAppender;
5 | import org.apache.commons.lang3.StringEscapeUtils;
6 | import org.slf4j.Logger;
7 | import org.slf4j.LoggerFactory;
8 | import org.springframework.web.filter.OncePerRequestFilter;
9 |
10 | import javax.servlet.*;
11 | import javax.servlet.http.HttpServletRequest;
12 | import javax.servlet.http.HttpServletResponse;
13 | import javax.servlet.http.HttpServletResponseWrapper;
14 | import java.io.CharArrayWriter;
15 | import java.io.IOException;
16 | import java.io.PrintWriter;
17 |
18 | /**
19 | * Created with IntelliJ IDEA.
20 | * User: will
21 | * Date: 11/24/13
22 | * Time: 5:08 PM
23 | */
24 | public class LogbackResponseServletFilter extends OncePerRequestFilter {
25 |
26 | private static final Logger logger = LoggerFactory.getLogger(LogbackResponseServletFilter.class);
27 |
28 |
29 | @Override
30 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
31 | boolean disabled = "true".equals(request.getParameter("disabled"));
32 | String url = request.getRequestURL().toString();
33 | boolean notHtml = url.endsWith(".js") || url.endsWith(".css");
34 |
35 | if (disabled || notHtml) {
36 | chain.doFilter(request, response);
37 | return;
38 | }
39 |
40 | CharResponseWrapper wrapper = new CharResponseWrapper(response);
41 | try {
42 | logger.trace("before doFilter()");
43 | chain.doFilter(request, wrapper);
44 | }
45 | finally {
46 | boolean doFilter = wrapper.getContentType() != null && wrapper.getContentType().contains("text/html");
47 | logger.debug("contentType: {}, filter this request? {}", wrapper.getContentType(), doFilter);
48 |
49 | if (!doFilter) {
50 | // do not modify response content
51 | return;
52 | }
53 |
54 | if (wrapper.toString() != null) {
55 | PrintWriter writer = response.getWriter();
56 | StringBuilder responseBody = new StringBuilder(wrapper.toString());
57 | logger.trace("responseBody before: {} bytes", responseBody.length());
58 |
59 | String encoded = ThreadLocalMemoryAppender.ThreadLocalHolder.getBufferAsJson(null, null);
60 | ThreadLocalMemoryAppender.ThreadLocalHolder.clearLoggedEvents();
61 | if (encoded != null) {
62 | StringBuilder div = new StringBuilder(encoded);
63 | logger.debug("html: {} bytes", div.length());
64 |
65 | int pos = responseBody.lastIndexOf("