Note that "@since 2.7.1" should all be "@since 2.8" Gary
---------- Forwarded message ---------- From: <mi...@apache.org> Date: Mon, Nov 14, 2016 at 2:17 AM Subject: [1/4] logging-log4j2 git commit: LOG4J2-1692: Add putAll() / pushAll() methods to CloseableThreadContext To: comm...@logging.apache.org Repository: logging-log4j2 Updated Branches: refs/heads/master 6e69e95d3 -> df481a19c LOG4J2-1692: Add putAll() / pushAll() methods to CloseableThreadContext Project: http://git-wip-us.apache.org/repos/asf/logging-log4j2/repo Commit: http://git-wip-us.apache.org/repos/asf/logging-log4j2/ commit/3ba3628f Tree: http://git-wip-us.apache.org/repos/asf/logging-log4j2/tree/3ba3628f Diff: http://git-wip-us.apache.org/repos/asf/logging-log4j2/diff/3ba3628f Branch: refs/heads/master Commit: 3ba3628fa6a94db512ceef10ecf2188ee740e857 Parents: abf29af Author: Greg Thomas <greg.d.tho...@gmail.com> Authored: Fri Nov 11 16:39:51 2016 +0000 Committer: Greg Thomas <greg.d.tho...@gmail.com> Committed: Fri Nov 11 16:39:51 2016 +0000 ---------------------------------------------------------------------- .../logging/log4j/CloseableThreadContext.java | 66 +++- .../log4j/CloseableThreadContextTest.java | 39 ++- src/site/xdoc/manual/thread-context.xml | 329 ++++++++++--------- 3 files changed, 272 insertions(+), 162 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/ 3ba3628f/log4j-api/src/main/java/org/apache/logging/log4j/ CloseableThreadContext.java ---------------------------------------------------------------------- diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/CloseableThreadContext.java b/log4j-api/src/main/java/org/apache/logging/log4j/ CloseableThreadContext.java index 9f0a279..5a45195 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/ CloseableThreadContext.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/ CloseableThreadContext.java @@ -62,8 +62,8 @@ public class CloseableThreadContext { } /** - * Populates the Thread Context Map with the supplied key/value pairs. Any existing keys in the - * {@link ThreadContext} will be replaced with the supplied values, and restored back to their original values when + * Populates the Thread Context Map with the supplied key/value pair. Any existing key in the + * {@link ThreadContext} will be replaced with the supplied value, and restored back to their original value when * the instance is closed. * * @param key The key to be added @@ -74,6 +74,31 @@ public class CloseableThreadContext { return new CloseableThreadContext.Instance().put(key, value); } + /** + * Populates the Thread Context Stack with the supplied stack. The information will be popped off when + * the instance is closed. + * + * @param values The stack of values to be added + * @return a new instance that will back out the changes when closed. + * @since 2.7.1 + */ + public static CloseableThreadContext.Instance pushAll(final ThreadContext.ContextStack stack) { + return new CloseableThreadContext.Instance().pushAll(stack); + } + + /** + * Populates the Thread Context Map with the supplied key/value pairs. Any existing keys in the + * {@link ThreadContext} will be replaced with the supplied values, and restored back to their original value when + * the instance is closed. + * + * @param values The map of key/value pairs to be added + * @return a new instance that will back out the changes when closed. + * @since 2.7.1 + */ + public static CloseableThreadContext.Instance putAll(final Map<String, String> values) { + return new CloseableThreadContext.Instance().putAll(values); + } + public static class Instance implements AutoCloseable { private int pushCount = 0; @@ -110,13 +135,13 @@ public class CloseableThreadContext { } /** - * Populates the Thread Context Map with the supplied key/value pairs. Any existing keys in the - * {@link ThreadContext} will be replaced with the supplied values, and restored back to their original values when + * Populates the Thread Context Map with the supplied key/value pair. Any existing key in the + * {@link ThreadContext} will be replaced with the supplied value, and restored back to their original value when * the instance is closed. * * @param key The key to be added * @param value The value to be added - * @return the instance that will back out the changes when closed. + * @return a new instance that will back out the changes when closed. */ public Instance put(final String key, final String value) { // If there are no existing values, a null will be stored as an old value @@ -128,6 +153,37 @@ public class CloseableThreadContext { } /** + * Populates the Thread Context Map with the supplied key/value pairs. Any existing keys in the + * {@link ThreadContext} will be replaced with the supplied values, and restored back to their original value when + * the instance is closed. + * + * @param values The map of key/value pairs to be added + * @return a new instance that will back out the changes when closed. + * @since 2.7.1 + */ + public Instance putAll(final Map<String, String> values) { + for (final Map.Entry<String, String> entry : values.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + return this; + } + + /** + * Populates the Thread Context Stack with the supplied stack. The information will be popped off when + * the instance is closed. + * + * @param values The stack of values to be added + * @return a new instance that will back out the changes when closed. + * @since 2.7.1 + */ + public Instance pushAll(final ThreadContext.ContextStack stack) { + for (final String message : stack.asList()) { + push(message); + } + return this; + } + + /** * Removes the values from the {@link ThreadContext}. * <p> * Values pushed to the {@link ThreadContext} <em>stack</em> will be popped off. http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/ 3ba3628f/log4j-api/src/test/java/org/apache/logging/log4j/ CloseableThreadContextTest.java ---------------------------------------------------------------------- diff --git a/log4j-api/src/test/java/org/apache/logging/log4j/ CloseableThreadContextTest.java b/log4j-api/src/test/java/org/ apache/logging/log4j/CloseableThreadContextTest.java index 1194678..6216c94 100644 --- a/log4j-api/src/test/java/org/apache/logging/log4j/ CloseableThreadContextTest.java +++ b/log4j-api/src/test/java/org/apache/logging/log4j/ CloseableThreadContextTest.java @@ -17,12 +17,16 @@ package org.apache.logging.log4j; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertThat; import org.apache.logging.log4j.junit.ThreadContextRule; import org.junit.Rule; import org.junit.Test; +import java.util.HashMap; +import java.util.Map; + /** * Tests {@link CloseableThreadContext}. * @@ -32,9 +36,9 @@ public class CloseableThreadContextTest { private final String key = "key"; private final String value = "value"; - + @Rule - public final ThreadContextRule threadContextRule = new ThreadContextRule(); + public final ThreadContextRule threadContextRule = new ThreadContextRule(); @Test public void shouldAddAnEntryToTheMap() throws Exception { @@ -184,4 +188,35 @@ public class CloseableThreadContextTest { assertThat(ThreadContext.get(key), is(originalMapValue)); assertThat(ThreadContext.peek(), is(originalStackValue)); } + + @Test + public void putAllWillPutAllValues() throws Exception { + + final String oldValue = "oldValue"; + ThreadContext.put(key, oldValue); + + final Map<String, String> valuesToPut = new HashMap<>(); + valuesToPut.put(key, value); + + try (final CloseableThreadContext.Instance ignored = CloseableThreadContext.putAll(valuesToPut)) { + assertThat(ThreadContext.get(key), is(value)); + } + assertThat(ThreadContext.get(key), is(oldValue)); + + } + + @Test + public void pushAllWillPushAllValues() throws Exception { + + ThreadContext.push(key); + final ThreadContext.ContextStack stack = ThreadContext. getImmutableStack(); + ThreadContext.pop(); + + try (final CloseableThreadContext.Instance ignored = CloseableThreadContext.pushAll(stack)) { + assertThat(ThreadContext.peek(), is(key)); + } + assertThat(ThreadContext.peek(), is("")); + + } + } http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/ 3ba3628f/src/site/xdoc/manual/thread-context.xml ---------------------------------------------------------------------- diff --git a/src/site/xdoc/manual/thread-context.xml b/src/site/xdoc/manual/thread-context.xml index dd5b21c..4401258 100644 --- a/src/site/xdoc/manual/thread-context.xml +++ b/src/site/xdoc/manual/thread-context.xml @@ -26,164 +26,183 @@ </properties> <body> - <section name="Log4j 2 API"> - <subsection name="Thread Context"> - <h4>Introduction</h4> - <p>Log4j introduced the concept of the Mapped Diagnostic Context or MDC. It has been documented and - discussed in numerous places including - <a href="http://veerasundar.com/blog/2009/10/log4j-mdc-mapped- diagnostic-context-what-and-why/">Log4j MDC: What and Why</a> and - <a href="http://blog.f12.no/wp/2004/12/09/log4j-and-the- mapped-diagnostic-context/">Log4j and the Mapped Diagnostic Context</a>. - In addition, Log4j 1.x provides support for a Nested Diagnostic Context or NDC. It too has been documented - and discussed in various places such as - <a href="http://lstierneyltd.com/blog/development/log4j-nested- diagnostic-contexts-ndc/">Log4j NDC</a>. - SLF4J/Logback followed with its own implementation of the MDC, which is documented very well at - <a href="http://logback.qos.ch/manual/mdc.html">Mapped Diagnostic Context</a>. - </p> - <p>Log4j 2 continues with the idea of the MDC and the NDC but merges them into a single Thread Context. - The Thread Context Map is the equivalent of the MDC and the Thread Context Stack is the equivalent of the - NDC. Although these are frequently used for purposes other than diagnosing problems, they are still - frequently referred to as the MDC and NDC in Log4j 2 since they are already well known by those acronyms. - </p> - <h4>Fish Tagging</h4> - <p>Most real-world systems have to deal with multiple clients simultaneously. In a typical multithreaded - implementation of such a system, different threads will handle different clients. Logging is - especially well suited to trace and debug complex distributed applications. A common approach to - differentiate the logging output of one client from another is to instantiate a new separate logger for - each client. This promotes the proliferation of loggers and increases the management overhead of logging. - </p> - <p>A lighter technique is to uniquely stamp each log request initiated from the same client interaction. - Neil Harrison described this method in the book "Patterns for Logging Diagnostic Messages," in <em>Pattern - Languages of Program Design 3</em>, edited by R. Martin, D. Riehle, and F. Buschmann - (Addison-Wesley, 1997). Just as a fish can be tagged and have its movement tracked, stamping log - events with a common tag or set of data elements allows the complete flow of a transaction or a request - to be tracked. We call this <i>Fish Tagging</i>. - </p> - <p>Log4j provides two mechanisms for performing Fish Tagging; the Thread Context Map and the Thread - Context Stack. The Thread Context Map allows any number of items to be added and be identified - using key/value pairs. The Thread Context Stack allows one or more items to be pushed on the - Stack and then be identified by their order in the Stack or by the data itself. Since key/value - pairs are more flexible, the Thread Context Map is recommended when data items may be added during - the processing of the request or when there are more than one or two items. - </p> - <p>To uniquely stamp each request using the Thread Context Stack, the user pushes contextual information - on to the Stack. - </p> - <pre class="prettyprint linenums"> -ThreadContext.push(UUID.randomUUID().toString()); // Add the fishtag; + <section name="Log4j 2 API"> + <subsection name="Thread Context"> + <h4>Introduction</h4> + <p>Log4j introduced the concept of the Mapped Diagnostic Context or MDC. It has been documented and + discussed in numerous places including + <a href="http://veerasundar.com/ blog/2009/10/log4j-mdc-mapped-diagnostic-context-what-and-why/">Log4j MDC: What and Why</a> and + <a href="http://blog.f12.no/wp/ 2004/12/09/log4j-and-the-mapped-diagnostic-context/">Log4j and the Mapped Diagnostic Context</a>. + In addition, Log4j 1.x provides support for a Nested Diagnostic Context or NDC. It too has been documented + and discussed in various places such as + <a href="http://lstierneyltd.com/ blog/development/log4j-nested-diagnostic-contexts-ndc/">Log4j NDC</a>. + SLF4J/Logback followed with its own implementation of the MDC, which is documented very well at + <a href="http://logback.qos.ch/manual/mdc.html">Mapped Diagnostic Context</a>. + </p> + <p>Log4j 2 continues with the idea of the MDC and the NDC but merges them into a single Thread Context. + The Thread Context Map is the equivalent of the MDC and the Thread Context Stack is the equivalent of the + NDC. Although these are frequently used for purposes other than diagnosing problems, they are still + frequently referred to as the MDC and NDC in Log4j 2 since they are already well known by those acronyms. + </p> + <h4>Fish Tagging</h4> + <p>Most real-world systems have to deal with multiple clients simultaneously. In a typical multithreaded + implementation of such a system, different threads will handle different clients. Logging is + especially well suited to trace and debug complex distributed applications. A common approach to + differentiate the logging output of one client from another is to instantiate a new separate logger for + each client. This promotes the proliferation of loggers and increases the management overhead of logging. + </p> + <p>A lighter technique is to uniquely stamp each log request initiated from the same client interaction. + Neil Harrison described this method in the book "Patterns for Logging Diagnostic Messages," in <em>Pattern + Languages of Program Design 3</em>, edited by R. Martin, D. Riehle, and F. Buschmann + (Addison-Wesley, 1997). Just as a fish can be tagged and have its movement tracked, stamping log + events with a common tag or set of data elements allows the complete flow of a transaction or a request + to be tracked. We call this <i>Fish Tagging</i>. + </p> + <p>Log4j provides two mechanisms for performing Fish Tagging; the Thread Context Map and the Thread + Context Stack. The Thread Context Map allows any number of items to be added and be identified + using key/value pairs. The Thread Context Stack allows one or more items to be pushed on the + Stack and then be identified by their order in the Stack or by the data itself. Since key/value + pairs are more flexible, the Thread Context Map is recommended when data items may be added during + the processing of the request or when there are more than one or two items. + </p> + <p>To uniquely stamp each request using the Thread Context Stack, the user pushes contextual information + on to the Stack. + </p> + <pre class="prettyprint linenums"> + ThreadContext.push(UUID.randomUUID().toString()); // Add the fishtag; -logger.debug("Message 1"); -. -. -. -logger.debug("Message 2"); -. -. -ThreadContext.pop();</pre> - <p> - The alternative to the Thread Context Stack is the Thread Context Map. In this case, attributes - associated with the request being processed are adding at the beginning and removed at the end - as follows: - </p> - <pre class="prettyprint linenums"> -ThreadContext.put("id", UUID.randomUUID().toString()); // Add the fishtag; -ThreadContext.put("ipAddress", request.getRemoteAddr()); -ThreadContext.put("loginId", session.getAttribute("loginId")); -ThreadContext.put("hostName", request.getServerName()); -. -logger.debug("Message 1"); -. -. -logger.debug("Message 2"); -. -. -ThreadContext.clear();</pre> + logger.debug("Message 1"); + . + . + . + logger.debug("Message 2"); + . + . + ThreadContext.pop();</pre> + <p> + The alternative to the Thread Context Stack is the Thread Context Map. In this case, attributes + associated with the request being processed are adding at the beginning and removed at the end + as follows: + </p> + <pre class="prettyprint linenums"> + ThreadContext.put("id", UUID.randomUUID().toString()); // Add the fishtag; + ThreadContext.put("ipAddress", request.getRemoteAddr()); + ThreadContext.put("loginId", session.getAttribute("loginId")); + ThreadContext.put("hostName", request.getServerName()); + . + logger.debug("Message 1"); + . + . + logger.debug("Message 2"); + . + . + ThreadContext.clear();</pre> - <h4>CloseableThreadContext</h4> - <p>When placing items on the stack or map, it's necessary to remove then again when appropriate. To assist with - this, the <tt>CloseableThreadContext</tt> implements the <a href="http://docs.oracle.com/javase/7/docs/api/java/lang/ AutoCloseable.html">AutoCloseable - interface</a>. This allows items to be pushed to the stack or put in the map, and removed when the <tt>close()</tt> method is called - - or automatically as part of a try-with-resources. For example, to temporarily push something on to the stack and then remove it: - </p> - <pre class="prettyprint linenums"> -// Add to the ThreadContext stack for this try block only; -try (final CloseableThreadContext.Instance ctc = CloseableThreadContext.push(UUID.randomUUID().toString())) { + <h4>CloseableThreadContext</h4> + <p>When placing items on the stack or map, it's necessary to remove then again when appropriate. To assist with + this, the <tt>CloseableThreadContext</tt> implements the <a href="http://docs.oracle.com/javase/7/docs/api/java/lang/ AutoCloseable.html">AutoCloseable + interface</a>. This allows items to be pushed to the stack or put in the map, and removed when the <tt>close()</tt> method is called - + or automatically as part of a try-with-resources. For example, to temporarily push something on to the stack and then remove it: + </p> + <pre class="prettyprint linenums"> + // Add to the ThreadContext stack for this try block only; + try (final CloseableThreadContext.Instance ctc = CloseableThreadContext.push(UUID.randomUUID().toString())) { - logger.debug("Message 1"); -. -. - logger.debug("Message 2"); -. -. -}</pre> - <p> - Or, to temporarily put something in the map: - </p> - <pre class="prettyprint linenums"> -// Add to the ThreadContext map for this try block only; -try (final CloseableThreadContext.Instance ctc = CloseableThreadContext.put("id", UUID.randomUUID().toString()) - .put("loginId", session.getAttribute("loginId"))) { + logger.debug("Message 1"); + . + . + logger.debug("Message 2"); + . + . + }</pre> + <p> + Or, to temporarily put something in the map: + </p> + <pre class="prettyprint linenums"> + // Add to the ThreadContext map for this try block only; + try (final CloseableThreadContext.Instance ctc = CloseableThreadContext.put("id", UUID.randomUUID().toString()) + .put("loginId", session.getAttribute("loginId"))) { - logger.debug("Message 1"); -. -. - logger.debug("Message 2"); -. -. -}</pre> - <h4>Implementation details</h4> - <p>The Stack and the Map are managed per thread and are based on - <a href="http://docs.oracle.com/javase/6/docs/api/java/lang/ ThreadLocal.html">ThreadLocal</a> - by default. The Map can be configured to use an - <a href="http://docs.oracle.com/javase/6/docs/api/java/lang/ InheritableThreadLocal.html">InheritableThreadLocal</a> - by setting system property <tt>isThreadContextMapInheritable</tt> to <tt>"true"</tt>. - When configured this way, the contents of the Map will be passed to child threads. However, as - discussed in the - <a href="http://docs.oracle.com/javase/6/docs/api/java/util/ concurrent/Executors.html#privilegedThreadFactory()">Executors</a> - class and in other cases where thread pooling is utilized, the ThreadContext may not always be - automatically passed to worker threads. In those cases the pooling mechanism should provide a means for - doing so. The getContext() and cloneStack() methods can be used to obtain copies of the Map and Stack - respectively. - </p> - <p> - Note that all methods of the - <a href="../log4j-api/apidocs/org/apache/logging/log4j/ ThreadContext.html">ThreadContext</a> - class are static. - </p> - <h4>Including the ThreadContext when writing logs</h4> - <p> - The <a href="../log4j-api/apidocs/ org/apache/logging/log4j/core/PatternLayout.html">PatternLayout</a> - provides mechanisms to print the contents of the - <a href="../log4j-api/apidocs/org/apache/logging/log4j/ ThreadContext.html">ThreadContext</a> - Map and Stack. - </p> - <ul> - <li> - Use <code>%X</code> by itself to include the full contents of the Map. - </li> - <li> - Use <code>%X{key}</code> to include the specified key. - </li> - <li> - Use <code>%x</code> to include the full contents of the <a href="http://docs.oracle.com/javase/6/docs/api/java/util/Stack.html ">Stack</a>. - </li> - </ul> - <h4>Custom context data injectors for non thread-local context data</h4> - <p> - With the ThreadContext logging statements can be tagged so log entries that were related in some way - can be linked via these tags. The limitation is that this only works for logging done on the same application thread - (or child threads when configured). - </p> - <p> - Some applications have a thread model that delegates work to other threads, and - in such models, tagging attributes that are put into a thread-local map in one thread are not visible - in the other threads and logging done in the other threads will not show these attributes. - </p> - <p> - Log4j 2.7 adds a flexible mechanism to tag logging statements with context data coming from - other sources than the ThreadContext. - See the manual page on <a href="extending.html#Custom_ContextDataInjector">extending Log4j</a> for details. - </p> - </subsection> - </section> + logger.debug("Message 1"); + . + . + logger.debug("Message 2"); + . + . + }</pre> + + If you're using a thread pool, then you can initialise a CloseableThreadContext by using the + <tt>putAll(final Map<String, String> values)</tt> method; + <pre class="prettyprint linenums"> + for( final Session session : sessions ) { + try (final CloseableThreadContext.Instance ctc = CloseableThreadContext.put("loginId", session.getAttribute("loginId"))) { + logger.debug("Starting background thread for user"); + final Map<String, String> values = ThreadContext. getImmutableContext(); + executor.submit(new Runnable() { + public void run() { + try (final CloseableThreadContext.Instance ctc = CloseableThreadContext.putAll(values)) { + logger.debug("Processing for user started"); + . + logger.debug("Processing for user completed"); + } + }); + } + }</pre> + + <h4>Implementation details</h4> + <p>The Stack and the Map are managed per thread and are based on + <a href="http://docs.oracle.com/ javase/6/docs/api/java/lang/ThreadLocal.html">ThreadLocal</a> + by default. The Map can be configured to use an + <a href="http://docs.oracle.com/ javase/6/docs/api/java/lang/InheritableThreadLocal.html"> InheritableThreadLocal</a> + by setting system property <tt> isThreadContextMapInheritable</tt> to <tt>"true"</tt>. + When configured this way, the contents of the Map will be passed to child threads. However, as + discussed in the + <a href="http://docs.oracle.com/ javase/6/docs/api/java/util/concurrent/Executors.html# privilegedThreadFactory()">Executors</a> + class and in other cases where thread pooling is utilized, the ThreadContext may not always be + automatically passed to worker threads. In those cases the pooling mechanism should provide a means for + doing so. The getContext() and cloneStack() methods can be used to obtain copies of the Map and Stack + respectively. + </p> + <p> + Note that all methods of the + <a href="../log4j-api/apidocs/org/apache/logging/log4j/ ThreadContext.html">ThreadContext</a> + class are static. + </p> + <h4>Including the ThreadContext when writing logs</h4> + <p> + The <a href="../log4j-api/apidocs/ org/apache/logging/log4j/core/PatternLayout.html">PatternLayout</a> + provides mechanisms to print the contents of the + <a href="../log4j-api/apidocs/org/apache/logging/log4j/ ThreadContext.html">ThreadContext</a> + Map and Stack. + </p> + <ul> + <li> + Use <code>%X</code> by itself to include the full contents of the Map. + </li> + <li> + Use <code>%X{key}</code> to include the specified key. + </li> + <li> + Use <code>%x</code> to include the full contents of the <a href="http://docs.oracle.com/javase/6/docs/api/java/util/ Stack.html">Stack</a>. + </li> + </ul> + <h4>Custom context data injectors for non thread-local context data</h4> + <p> + With the ThreadContext logging statements can be tagged so log entries that were related in some way + can be linked via these tags. The limitation is that this only works for logging done on the same application thread + (or child threads when configured). + </p> + <p> + Some applications have a thread model that delegates work to other threads, and + in such models, tagging attributes that are put into a thread-local map in one thread are not visible + in the other threads and logging done in the other threads will not show these attributes. + </p> + <p> + Log4j 2.7 adds a flexible mechanism to tag logging statements with context data coming from + other sources than the ThreadContext. + See the manual page on <a href="extending.html#Custom_ContextDataInjector">extending Log4j</a> for details. + </p> + </subsection> + </section> </body> </document> -- E-Mail: garydgreg...@gmail.com | ggreg...@apache.org Java Persistence with Hibernate, Second Edition <https://www.amazon.com/gp/product/1617290459/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=1617290459&linkCode=as2&tag=garygregory-20&linkId=cadb800f39946ec62ea2b1af9fe6a2b8> <http:////ir-na.amazon-adsystem.com/e/ir?t=garygregory-20&l=am2&o=1&a=1617290459> JUnit in Action, Second Edition <https://www.amazon.com/gp/product/1935182021/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=1935182021&linkCode=as2&tag=garygregory-20&linkId=31ecd1f6b6d1eaf8886ac902a24de418%22> <http:////ir-na.amazon-adsystem.com/e/ir?t=garygregory-20&l=am2&o=1&a=1935182021> Spring Batch in Action <https://www.amazon.com/gp/product/1935182951/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=1935182951&linkCode=%7B%7BlinkCode%7D%7D&tag=garygregory-20&linkId=%7B%7Blink_id%7D%7D%22%3ESpring+Batch+in+Action> <http:////ir-na.amazon-adsystem.com/e/ir?t=garygregory-20&l=am2&o=1&a=1935182951> Blog: http://garygregory.wordpress.com Home: http://garygregory.com/ Tweet! http://twitter.com/GaryGregory