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-loggingorcommons-logging-apidependencies - Ensures no
log4jdependencies - Ensures no
SLF4J1.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
Hi,
Concerning the “Never use JUL, use SLF4J instead”, I would add the things to set up to override default JUL logging (since JUL is in the JDK, we cannot bridge it easily… this is the reason why dependency is named jul-TO-slf4j and not jul-OVER-slf4j)
More info on setting up slf4j to intercept JUL logging : http://stackoverflow.com/q/9117030
Thanks Frederic!
I will do that in the next blog.
Regards,
Gordon Dickens
twitter.com/gdickens
linkedin.com/in/gordondickens
GitHub: github.com/gordonad
I think best practice is to put all this ancillary project setup in a parent pom. This way its reusable for different projects, and doesn’t bloat the actually project pom.
Pingback: Enterprise Spring Best Practices – Part 2 – Application Architecture | Technophile Blog
Pingback: Enterprise Spring Framework Best Practices – Part 3 – XML Config | Technophile Blog
Hi,
The documentation for the enforcer plugin’s bannedDependencies tag has the following format specification for exclusions :
“The format is groupId[:artifactId][:version][:type][:scope] where artifactId, version, type, and scope are optional. Wildcards may be used to replace an entire section.”
If I interpret the above correctly, the following exclusion is correct:
org.springframework:*:1.2
but, this is not:
org.springframework:1.*
The wildcard seems to be meant only to replace entire sections and cannot be used to specify revision ranges.
My experiments seem to concur with the above interpretation.
This is what I am using currently:
true
org.springframework
org.springframework:*:3.1.2
true
true
true
The configuration I am using currently for the enforcer plugin is as in this question :
http://stackoverflow.com/questions/13156428/is-this-the-correct-way-to-enforce-a-specific-spring-version-using-the-enforcer
Pingback: Enterprise Spring Best Practices – Part 4 – Annotation Config | Technophile Blog
The jmx through localhost:8050 does not seem to work. Am I missing something or is jmx not fully supported by this config?
BTW, thanks for a great guide of best practices!
Pingback: Sawing through the Java Loggers | Technophile Blog