Enterprise Spring Best Practices Series
In part 1, let’s review project structure and configuration.
Sections
- Project Directories
- Project Dependencies
- Smart Logging
- Running with Jetty and Tomcat
- Spring Configuration Files
- Complete Maven Config
- Valuable Maven Commands
- Further Reading
- Social Me
Project Directories
src/main/java
– Java Source code packages and classessrc/main/resources
– NON-Java Resources, such as property files and Spring configuration
src/test/java
– Test Source code packages and classessrc/test/resources
– NON-Java Resources, such as property files and Spring configuration
Project Structure Example
── pom.xml └── src ├── main │ ├── java │ │ └── com │ │ └── gordondickens │ │ └── sample │ │ ├── domain │ │ │ └── MyDomain.java │ │ ├── repository │ │ │ └── MyDomainRepository.java │ │ ├── service │ │ │ ├── MyDomainService.java │ │ │ └── internal │ │ │ └── MyDomainServiceImpl.java │ │ └── web │ │ └── MyDomainController.java │ ├── resources │ │ ├── META-INF │ │ │ └── spring │ │ │ ├── applicationContext.xml │ │ │ └── database.properties │ │ ├── logback-access.xml │ │ └── logback.xml │ └── webapp │ ├── WEB-INF │ │ ├── classes │ │ ├── i18n │ │ ├── layouts │ │ ├── spring │ │ │ └── webmvc-config.xml │ │ ├── views │ │ │ ├── myDomain │ │ │ │ ├── create.jsp │ │ │ │ ├── list.jsp │ │ │ │ ├── show.jsp │ │ │ │ └── update.jsp │ │ │ ├── dataAccessFailure.jsp │ │ │ ├── index.jsp │ │ │ ├── resourceNotFound.jsp │ │ │ ├── uncaughtException.jsp │ │ │ └── views.xml │ │ └── web.xml │ ├── images │ └── styles ├── site │ ├── apt │ ├── fml │ ├── site.xml │ └── xdoc └── test ├── java │ └── com │ └── gordondickens │ └── sample │ └── service │ └── MyDomainServiceTests.java └── resources ├── com │ └── gordondickens │ └── sample │ └── service │ └── MyDomainServiceTests-context.xml └── logback-test.xml
Project Dependencies
I am a big fan of Maven, it provides a consistent build structure and numerous plugins. Gradle is emerging as an alternative Groovy-based build tool, which supports the Maven structure. If you are still using Ant, I urge you to move to a more robust build tool such as Maven or Gradle. One of the challenges of enterprise build tools is managing transitive dependencies, here are some recommendations to ease these challenges.
- DO NOT put version numbers below the
<properties/>
section, this will make it easier to upgrade and test newer versions. - DO include version numbers for ALL plugins! Do not rely on Maven’s built in Super Pom plugin versions!
- USE Maven’s
<DependencyManagement>
section to control implicit and explicit versions! Transitive dependencies will be resolved by those included in this section.
Prohibit the direct or indirect inclusion of incompatible and/or legacy jars. For example, SLF4J 1.5, 1.6 and SLF4J 1.7 do not work together, therefore we need to prohibit the project from building with mixed dependency versions. Spring is used by many open source projects, some reference older versions of Spring jars, so we want to control which Spring jar versions are inluded in our build.
Enforcer Example
- Ensures Java 1.6
- Ensures Maven 2.2.1 to 3.0.x
- Ensures Spring Jars 3.1 or greater
- Prohibits old
javassist
, should beorg.javassist
- Ensures no
commons-logging
orcommons-logging-api
dependencies - Ensures no
log4j
dependencies - Ensures no
SLF4J
1.5 or 1.6 dependencies - Prohibits old
hsqldb
, should beorg.hsqldb
- Prohibits old
aspectj
, should beorg.aspectj
<properties> ... <java.version>1.6</java.version> ... <maven.enforcer.plugin>1.2</maven.enforcer.plugin> <maven.version.range>[2.2.1,3.1.0)</maven.version.range> ... </properties> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-enforcer-plugin</artifactId> <version>${maven.enforcer.plugin}</version> <executions> <execution> <id>enforce-banned-dependencies</id> <goals> <goal>enforce</goal> </goals> <configuration> <rules> <bannedDependencies> <searchTransitive>true</searchTransitive> <excludes> <exclude>javassist:javassist</exclude> <exclude>commons-logging</exclude> <exclude>aspectj:aspectj*</exclude> <exclude>hsqldb:hsqldb</exclude> <exclude>log4j:log4j</exclude> <exclude>org.slf4j:1.5*</exclude> <exclude>org.springframework:2.*</exclude> <exclude>org.springframework:3.0.*</exclude> </excludes> </bannedDependencies> <requireMavenVersion> <version>${maven.version.range}</version> </requireMavenVersion> <requireJavaVersion> <version>${java.version}</version> </requireJavaVersion> </rules> <fail>true</fail> </configuration> </execution> </executions> </plugin>
Smart Logging
- NEVER use
System.out
- NEVER use
System.err
- ALWAYS use SLF4J
- ALWAYS use Logback
- Prohibit Apache Commons Logging (JCL) aka Jakarta Commons Logging
- Prohibit Java Util Logging (JUL)
Classes that use logging should include the following config for SLF4J (not log4j, not jcl, not jul, not logback):
import org.slf4j.Logger; import org.slf4j.LoggerFactory; ... public class MyClass { private static final Logger logger = LoggerFactory.getLogger(MyClass.class); ... }
In the example below, SLF4J provides jars to route JCL and JUL logging through jcl-over-slf4j
and jul-to-slf4j
. Spring uses JCL, so we need to use jcl-over-slf4j
to handle Spring specific logged messages.
<properties> ... <logback.version>1.0.10</logback.version> ... <slf4j.version>1.7.4</slf4j.version> ... </properties> ... <dependencies> ... <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jul-to-slf4j</artifactId> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> </dependency> ... </dependencies> ... <dependencyManagement> <dependencies> ... <!-- Logging with SLF4J & LogBack --> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jul-to-slf4j</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>${logback.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-access</artifactId> <version>${logback.version}</version> </dependency> ... </dependencies> </dependencyManagement>
src/main/resources/logback.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration> <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"> <resetJUL>true</resetJUL> </contextListener> <!-- To enable JMX Management --> <jmxConfigurator/> <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%.-1level|%-40.40logger{0}|%msg%n</pattern> </encoder> </appender> <logger name="com.mycompany.myapp" level="debug" /> <logger name="org.springframework" level="info" /> <logger name="org.springframework.beans" level="debug" /> <root level="warn"> <appender-ref ref="console" /> </root> </configuration>
src/main/resources/logback-test.xml
Same configuration as production code, that will only be used for tests, usually for adding more log detail.
src/main/resources/logback-access.xml
Configuration for server access logs. HTTPRequest
and HTTPResponses
messages can be displayed and/or logged When used with Logback TeeFilter in web.xml – GREAT for RESTful testing.
NOTE: Using ${user.dir}
, the log files will be created in the root of the project. We will want to configure this differently for production.
<?xml version="1.0" encoding="UTF-8"?> <configuration> <statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" /> <filter class="ch.qos.logback.access.filter.CountingFilter"> <name>countingFilter</name> </filter> <appender name="accessfile" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${user.dir}/logs/app-access.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${user.dir}/logs/app-access.%d{yyyy-MM-dd}.log.zip</fileNamePattern> </rollingPolicy> <encoder> <pattern>combined</pattern> </encoder> </appender> <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%n%fullRequest%n%fullResponse%n</pattern> </encoder> </appender> <appender name="reqrespfile" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${user.dir}/logs/app-req-resp.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${user.dir}/logs/app-req-resp.%d{yyyy-MM-dd}.log.zip</fileNamePattern> </rollingPolicy> <encoder> <pattern>%n%fullRequest%n%fullResponse%n</pattern> </encoder> </appender> <appender-ref ref="accessfile" /> <appender-ref ref="reqrespfile" /> <appender-ref ref="console" /> </configuration>
See Enterprise Spring Best Practices – XML Configuration – Part 3 for Spring configuration of java.util.logging
and System.out
, System.err
handlers for SLF4J.
Running with Jetty and Tomcat
Developers can run Jetty or Tomcat for testing with the following Maven plugin configuration. The plugin configuration below configures the servers for JMX, SLF4J, Logback and Logback Access.
$ mvn clean install jetty:run
$ mvn clean install tomcat7:run
NOTE: DO NOT use tomcat:run
, this is the old Tomcat plugin.
<properties> ... <maven.jetty.plugin>8.1.10.v20130312</maven.jetty.plugin> ... <maven.tomcat.plugin>2.1</maven.tomcat.plugin> ... </properties> ... <plugins> ... <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>${maven.tomcat.plugin}</version> <configuration> <systemProperties> <com.sun.management.jmxremote>true</com.sun.management.jmxremote> <com.sun.management.jmxremote.port>8050</com.sun.management.jmxremote.port> <com.sun.management.jmxremote.ssl>false</com.sun.management.jmxremote.ssl> <com.sun.management.jmxremote.authenticate>false</com.sun.management.jmxremote.authenticate> <java.util.logging.manager>org.apache.juli.ClassLoaderLogManager</java.util.logging.manager> <logback.ContextSelector>JNDI</logback.ContextSelector> </systemProperties> </configuration> <dependencies> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>${slf4j.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jul-to-slf4j</artifactId> <version>${slf4j.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-access</artifactId> <version>${logback.version}</version> <scope>runtime</scope> </dependency> </dependencies> </plugin> <plugin> <groupId>org.mortbay.jetty</groupId> <artifactId>jetty-maven-plugin</artifactId> <version>${maven.jetty.plugin}</version> <configuration> <webAppConfig> <contextPath>/${project.name}</contextPath> </webAppConfig> <stopPort>9966</stopPort> <stopKey>shutterdown</stopKey> <requestLog implementation="ch.qos.logback.access.jetty.RequestLogImpl"> <fileName>./src/main/resources/logback-access.xml</fileName> </requestLog> <systemProperties> <systemProperty> <name>logback.configurationFile</name> <value>./src/main/resources/logback.xml</value> </systemProperty> <systemProperty> <name>com.sun.management.jmxremote</name> <value>true</value> </systemProperty> <systemProperty> <name>com.sun.management.jmxremote.port</name> <value>8050</value> </systemProperty> <systemProperty> <name>com.sun.management.jmxremote.ssl</name> <value>false</value> </systemProperty> <systemProperty> <name>com.sun.management.jmxremote.authenticate</name> <value>false</value> </systemProperty> </systemProperties> </configuration> <dependencies> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>${slf4j.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jul-to-slf4j</artifactId> <version>${slf4j.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-access</artifactId> <version>${logback.version}</version> <scope>runtime</scope> </dependency> </dependencies> </plugin> ... </plugins>
web.xml
HelpersTo see Logback status, optionally add the following Logback Status servlet.
... <servlet> <servlet-name>ViewStatusMessages</servlet-name> <servlet-class>ch.qos.logback.classic.ViewStatusMessagesServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>ViewStatusMessages</servlet-name> <url-pattern>/logbackStatus</url-pattern> </servlet-mapping> ...
To capture the HTTPRequest
and HTTPResponse
data use the Logback Tee Filter.
... <filter> <filter-name>TeeFilter</filter-name> <filter-class>ch.qos.logback.access.servlet.TeeFilter</filter-class> </filter> <filter-mapping> <filter-name>TeeFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> ...
Spring Configuration Files
Be consistent with naming Spring xml configuration files. Start all files with the same name such as
applicationConfig*.xml
.
For example: applicationConfig-bootstrap.xml
, applicationConfig-jpa.xml
, applicationConfig-security.xml
, etc.
In the next blog, I will discuss Enterprise Spring configuration best practices.
src/main/resources/META-INF/spring
– Spring XML configuration directorysrc/main/webapp/WEB-INF/spring
– Spring MVC configuration directory
Complete Maven Config
The Best Practices Maven Config file is tuned for Spring application dependencies, reporting and plugin support.
Features of the Best Practices Maven Config file:
- All versions in properties section
- Dependency Management section controls transitive dependencies
- All plugins defined with versions in Plugin Management section
- Enforcer plugin stops build for incompatible dependencies
- Maven Site plugin configured for reporting, with common reporting plugins
- Eclipse plugin uses new Eclipse brand Maven plugin, formerly Sonatype’s
- Idea (IntelliJ) plugin, is obsolete – not included
- Versions plugin to check for dependency and plugin updates
BEST Practices Maven Config File
Valuable Maven Commands
$ mvn versions:display-dependency-updates
$ mvn versions:display-plugin-updates
$ mvn dependency:tree -Ddetail
$ mvn dependency:list
$ mvn help:effective-pom
$ mvn help:effective-settings
$ mvn help:system
$ mvn dependency:build-classpath
Further Reading
- Enterprise Spring Best Practices – Application Architecture – Part 2
- Enterprise Spring Best Practices – XML Configuration – Part 3
- Enterprise Spring Best Practices – Source Code
- FREE Spring Framework PDF (848 pages)
- Apache Maven
- SLF4J
- Logback
- Gradle
Social Me
Twitter – twitter.com/gdickens
LinkedIn – linkedin.com/in/gordondickens
GitHub: github.com/gordonad
gordon@gordondickens.com