Log4j将不同Package的日志输出到不同的文件的方法

随着项目规模的越来越大,会不断的引入新的模块,不同的模块都会打印自己的日志,最后就造成日志根本没法查看,比如我自己的项目中,就存在以下这些日志:

  1. 接收外界消息的日志、对外发送消息的日志;
  2. 后台常驻线程的处理日志;
  3. 外部接口访问的参数、返回结果等接口日志;
  4. Service访问数据库产生的SQL日志;

这其中,消息日志和后台线程的日志数据量非常庞大,如果所有日志打印在一个文件中,使用tail -f log.log文件,会发现日志在快速的滚动,根本无法查看甚至定位某一个具体的SQL或者Service访问日志。

解决方法就是可以将不同的日志加以分类输出,这样相互的日志不影响,尤其重要的接口访问日志,能够很方便的定位和排查问题。

步骤1:在log4j.properties中配置

先贴一下我自己所有的log4j.properties配置:

log4j.rootLogger=INFO, console, file

log4j.appender.console=net.czt.log.AsyncConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d [%t] %-5p crazyant-web %-17c{2} (%13F:%L) %X{USER_ID}|%X{USER_IP}|%X{SERVER_ADDRESS}|%X{SERVER_NAME}|%X{REQUEST_URI}|%X{SESSION_ID} - %m%n
log4j.appender.console.bufferSize=10000
log4j.appender.console.encoding=UTF-8

log4j.appender.file=org.apache.log4j.RollingFileAppender
log4j.appender.file.file=/home/work/apache-tomcat-6.0.39/logs/crazyant.log
log4j.appender.file.MaxBackupIndex=5
log4j.appender.file.MaxFileSize=1GB
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%-5p] crazyant-web %d{yyyy-MM-dd HH:mm:ss,SSS} %X{USER_ID}|%X{USER_IP}|%X{SERVER_ADDRESS}|%X{SERVER_NAME}|%X{REQUEST_URI}|%X{SESSION_ID} method:%l%n%m%n
log4j.appender.file.bufferSize=10000
log4j.appender.file.encoding=UTF-8

log4j.logger.net.czt.crazyant.msg=DEBUG, message
log4j.additivity.net.czt.crazyant.msg=false
log4j.appender.message=org.apache.log4j.RollingFileAppender
log4j.appender.message.File=/home/work/apache-tomcat-6.0.39/logs/crazyant_message.log
log4j.appender.message.Append=true
log4j.appender.message.MaxFileSize=1GB
log4j.appender.message.MaxBackupIndex=5
log4j.appender.message.layout=org.apache.log4j.PatternLayout
log4j.appender.message.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%-5p][%c{1}] [%t] - %m%n
log4j.appender.message.encoding=UTF-8

log4j.logger.net.czt.crazyant.async.service=DEBUG, async
log4j.additivity.net.czt.crazyant.async.service=false
log4j.appender.async=org.apache.log4j.RollingFileAppender
log4j.appender.async.File=/home/work/apache-tomcat-6.0.39/logs/crazyant_async.log
log4j.appender.async.Append=true
log4j.appender.async.MaxFileSize=1GB
log4j.appender.async.MaxBackupIndex=5
log4j.appender.async.layout=org.apache.log4j.PatternLayout
log4j.appender.async.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%-5p][%c{1}] [%t] - %m%n
log4j.appender.async.encoding=UTF-8

log4j.logger.net.czt.orm.mybatis.SqlMonitorManager=DEBUG, showsql
log4j.additivity.net.czt.orm.mybatis.SqlMonitorManager=false
log4j.logger.net.czt.transaction.interceptor.SmartTransactionInterceptor=DEBUG, showsql
log4j.additivity.net.czt.transaction.interceptor.SmartTransactionInterceptor=false
log4j.appender.showsql=org.apache.log4j.RollingFileAppender
log4j.appender.showsql.File=/home/work/apache-tomcat-6.0.39/logs/crazyant_sql.log
log4j.appender.showsql.Append=true
log4j.appender.showsql.MaxFileSize=1GB
log4j.appender.showsql.MaxBackupIndex=5
log4j.appender.showsql.layout=org.apache.log4j.PatternLayout
log4j.appender.showsql.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%-5p][%c{1}] [%t] - %m%n
log4j.appender.showsql.encoding=UTF-8

log4j.logger.net.czt.crazyant.service=DEBUG, service
log4j.additivity.net.czt.crazyant.service=false
log4j.appender.service=org.apache.log4j.RollingFileAppender
log4j.appender.service.File=/home/work/apache-tomcat-6.0.39/logs/crazyant_service.log
log4j.appender.service.Append=true
log4j.appender.service.MaxFileSize=1GB
log4j.appender.service.MaxBackupIndex=5
log4j.appender.service.layout=org.apache.log4j.PatternLayout
log4j.appender.service.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%-5p][%c{1}] [%t] - %m%n
log4j.appender.service.encoding=UTF-8

 

在配置文件的下方,可以方便的看到,我将message(消息)、async(后端线程)、showsql(数据库日志)、service(接口调用)分别输出到了不同的日志文件。

其中的一些解释:

log4j.rootLogger=INFO, console, file

log4j有一个rootLogger和普通Logger的概念,默认情况下我们只需要一个rootLogger,就是所有的日志只会输出到这一个日志文件中。

 

看一下普通Logger的配置(以接口日志service为例):

  • log4j.logger.net.czt.crazyant.service=DEBUG, service
    • 这句中的”net.czt.crazyant.service”,表示该普通logger日志配置生效的package的完全路径
    • 其中色service,表示该普通logger的名字
  • log4j.additivity.net.czt.crazyant.service=false
    • 其中的”net.czt.crazyant.service”,和上面的相同,表示该配置项针对的package
    • 该句配置的意思,是不要将该package的日志输出到rootLogger日志中,只输出到自己配置的日志就行了;
  • log4j.appender.service=org.apache.log4j.RollingFileAppender,以及该配置段下面的配置项
    • 这里的”service”字符串,和上面的第一个配置项的”service”相同,表示对该普通Logger的配置;
    • 下方的配置项和rootLogger相同,表示每天输出文件、编码UTF8、分片规则、每行的输出模式等等

我自己遇到的问题,是上面的log4j.properties配置好以后,发现各个日志文件创建了,但是里面都没有内容,这是为啥呢?来看下面第二个注意的地方;

步骤2、输出日志时需要设定日志对象对应的具体Class

什么意思呢?上面的配置项中,有一个”net.czt.crazyant.service”的package字符串,那么我们自己想一下,log4j是怎样将不同package中的logger日志输出到不同文件呢,想一下会有两种方法:

  1. 采用intercepter或者aop的方式,log4j自己检测日志输出,检测到日志产生于哪个package,就将其输出到对应文件中;
  2. 由用户传一个Class参数,log4j获取该Class对应的Package,以此为准,来定位不同的日志文件;

看一下代码,显然log4j用的是后一种简单直接的方式:

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class MyClassImpl implements MyClass {
    /**
     * loger
     */
    private static final Log logger = LogFactory.getLog(MyClassImpl.class);
    
    /**
     * my func
     */
    public void myfunc() {
        logger.info("call method myfunc.");
    }
}

在logger = LogFactory.getLog(MyClassImpl.class)中,传入了使用该logger的Class参数,而该Class被反射取到的package地址,就是log4j用来输出日志的package地址。

这种做法也有强大的地方,方便逻辑上的日志归类,比如很多代码不属于一个package,但是它们逻辑上属于一起的,举个例子,消息的处理不只是接口调用Service这个package,可能还会调用发送msg的操作,如果想把msg的package中一些日志也输出到Service,那么在这个msg的logger初始化的时候,传入一个Serivice的Class就行了。

或者对于某一类的所有日志来说,它们所有的logger对象,都来自封装好的单个对象实例即可,而这个单个对象实例传入的参数只有一个,用于标识这个逻辑归类即可。

总结

在Log4j.properties中,支持package或者具体class的日志单独输出,但是也需要代码中logger初始化的时候,能和日志配置中的package对应上。

 

原创文章,转载请注明地址:http://crazyant.net/1931.html

将Maven工程打包成可执行JAR包的方法

如果项目中有需要后台执行的任务,但是主要的逻辑都在Java代码中,那么我采用的方式是单独建立一个maven模块打成jar包,然后在linux后台通过命令执行Jar包的Main函数:

java -classpath backtask.jar "net.crazyant.RunWebService"

于是就有个前提,得将maven模块打包成jar包,原本很简单的事情,却出现了很多问题。

原始打包方式:使用maven-assembly-plugin

POM配置片段为:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
        </plugin>   
        <plugin>
            <artifactId>maven-assembly-plugin</artifactId>
            <version>2.2.1</version>
            <configuration>
                <finalName>mdm-v3-backtasks</finalName>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
            </configuration>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>single</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-resources-plugin</artifactId>
            <version>2.6</version>
            <configuration>
                <encoding>UTF-8</encoding>
            </configuration>
        </plugin> 
    </plugins>
</build>

这种打包方式,可以生成/target/mdm-v3-backtasks-1.0.0.2.jar的Jar包,在有些工程中是没有问题的,但是我的新工程打包后,却在运行时出现了如下问题:

Exception in thread "main" org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Unable to locate Spring NamespaceHandler for XML schema namespace [http://www.springframework.org/schema/context]
Offending resource: class path resource [applicationContext-backtasks.xml]

        at org.springframework.beans.factory.parsing.FailFastProblemReporter.error(FailFastProblemReporter.java:68)
        at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:85)
        at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:80)
        at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.error(BeanDefinitionParserDelegate.java:318)
        at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1435)
        at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1428)
        at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:195)
        at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:139)
        at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.registerBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:108)
        at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.registerBeanDefinitions(XmlBeanDefinitionReader.java:493)
        at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:390)
        at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:334)
        at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:302)
        at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:174)
        at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:209)
        at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:180)
        at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:243)
        at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:127)
        at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:93)
        at org.springframework.context.support.AbstractRefreshableApplicationContext.refreshBeanFactory(AbstractRefreshableApplicationContext.java:130)
        at org.springframework.context.support.AbstractApplicationContext.obtainFreshBeanFactory(AbstractApplicationContext.java:537)
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:451)
        at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:139)
        at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83)

网上搜了很多文章,发现首先大家都支持的,是maven-assembly-plugin插件,替换成maven-shade-plugin插件。

但是替换成maven-shade-plugin之后,仍然遇到了很多问题:

使用maven-shade-plugin逐步解决问题

使用maven-shade-plugin插件,并没有一下子就把问题解决了,也经过了很多步骤:

1、修改spring.xml的schema编写方式

原始的声明是这样的:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd">

作如下修改,把xsd的版本加上:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context-3.0.xsd">

2、引入maven-shade-plugin插件的打包方式遇到的问题

将原来的maven-assembly-plugin替换成新的打包方式:

<build>
	<finalName>mdm-v3-backtasks</finalName>
	<plugins>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-jar-plugin</artifactId>
		</plugin>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-shade-plugin</artifactId>
			<version>2.4.2</version>
			<executions>
				<execution>
					<phase>package</phase>
					<goals>
						<goal>shade</goal>
					</goals>
				</execution>
			</executions>
		</plugin>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-resources-plugin</artifactId>
			<version>2.6</version>
			<configuration>
				<encoding>UTF-8</encoding>
			</configuration>
		</plugin>
	</plugins>
</build>

然后就报了下面的错:

Exception in thread "main" java.lang.SecurityException: Invalid signature file digest for Manifest main attributes
        at sun.security.util.SignatureFileVerifier.processImpl(SignatureFileVerifier.java:286)
        at sun.security.util.SignatureFileVerifier.process(SignatureFileVerifier.java:239)
        at java.util.jar.JarVerifier.processEntry(JarVerifier.java:317)
        at java.util.jar.JarVerifier.update(JarVerifier.java:228)
        at java.util.jar.JarFile.initializeVerifier(JarFile.java:348)
        at java.util.jar.JarFile.getInputStream(JarFile.java:415)
        at sun.misc.URLClassPath$JarLoader$2.getInputStream(URLClassPath.java:775)
        at sun.misc.Resource.cachedInputStream(Resource.java:77)
        at sun.misc.Resource.getByteBuffer(Resource.java:160)
        at java.net.URLClassLoader.defineClass(URLClassLoader.java:436)
        at java.net.URLClassLoader.access$100(URLClassLoader.java:71)
        at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
        at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:425)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:358)
        at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:482)

在http://zhentao-li.blogspot.com/2012/06/maven-shade-plugin-invalid-signature.html找到了解决方法:

You need to add the following to pom.xml:

        <configuration>
          <filters>
            <filter>
              <artifact>*:*</artifact>
              <excludes>
                <exclude>META-INF/*.SF</exclude>
                <exclude>META-INF/*.DSA</exclude>
                <exclude>META-INF/*.RSA</exclude>
              </excludes>
            </filter>
          </filters>
        </configuration>

于是加入以上的filter,新的POM内容为:

<build>
	<finalName>mdm-v3-backtasks</finalName>
	<plugins>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-jar-plugin</artifactId>
		</plugin>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-shade-plugin</artifactId>
			<version>2.4.2</version>
			<executions>
				<execution>
					<phase>package</phase>
					<goals>
						<goal>shade</goal>
					</goals>
					<configuration>
						<filters>
							<filter>
								<artifact>*:*</artifact>
								<excludes>
									<exclude>META-INF/*.SF</exclude>
									<exclude>META-INF/*.DSA</exclude>
									<exclude>META-INF/*.RSA</exclude>
								</excludes>
							</filter>
						</filters>
					</configuration>
				</execution>
			</executions>
		</plugin>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-resources-plugin</artifactId>
			<version>2.6</version>
			<configuration>
				<encoding>UTF-8</encoding>
			</configuration>
		</plugin>
	</plugins>
</build>

修改后提交,不幸的是,又爆出了下面的错误:

Exception in thread "main" org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Unable to locate Spring NamespaceHandler for XML schema namespace [http://www.springframework.org/schema/context]
Offending resource: class path resource [applicationContext-backtasks.xml]

        at org.springframework.beans.factory.parsing.FailFastProblemReporter.error(FailFastProblemReporter.java:68)
        at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:85)
        at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:80)
        at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.error(BeanDefinitionParserDelegate.java:318)
        at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1435)
        at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1428)
        at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:195)
        at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:139)
        at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.registerBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:108)
        at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.registerBeanDefinitions(XmlBeanDefinitionReader.java:493)
        at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:390)
        at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:334)
        at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:302)
        at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:174)
        at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:209)
        at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:180)
        at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:243)
        at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:127)
        at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:93)
        at org.springframework.context.support.AbstractRefreshableApplicationContext.refreshBeanFactory(AbstractRefreshableApplicationContext.java:130)
        at org.springframework.context.support.AbstractApplicationContext.obtainFreshBeanFactory(AbstractApplicationContext.java:537)
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:451)
        at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:139)
        at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83)

最后在http://robert-reiz.com/2011/11/14/832/页面找到了答案:

这是因为项目中包含了很多的Spring Jar,不同的Spring Jar会有相同的文件名称,他们相互冲突,为了避免元数据文件的相互覆盖,应该合并他们,如果使用maven shade plugin的话,可以在POM中加上下面的信息来解决:

<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
  <resource>META-INF/spring.handlers</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
  <resource>META-INF/spring.schemas</resource>
</transformer>

对该问题,官方的解释位于:http://maven.apache.org/plugins/maven-shade-plugin/examples/resource-transformers.html

在“Merging Content of Specific Files with AppendingTransformer and XmlAppendingTransformer”小节,解释为:

很多的JAR包含了相同的文件名称,为了避免相互覆盖,可以将他们合并到单个文件中。一个很好的例子,就是spring-context包和plexus-spring包,他俩都有META-INF/spring.handlers文件,这个文件被Spring用来处理XML Schema namespaces,通过如下所示的merge方法,可以解决这个问题

问题终于找到了,原来在这里,看下官方贴出来的POM:

<project>
  ...
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>2.4.2</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <transformers>
                <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                  <resource>META-INF/spring.handlers</resource>
                </transformer>
                <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                  <resource>META-INF/spring.schemas</resource>
                </transformer>
              </transformers>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
  ...
</project>

最终的解决方案

最终的POM文件如下所示:

<build>
	<finalName>mdm-v3-backtasks</finalName>
	<plugins>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-jar-plugin</artifactId>
		</plugin>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-shade-plugin</artifactId>
			<version>2.4.2</version>
			<executions>
				<execution>
					<phase>package</phase>
					<goals>
						<goal>shade</goal>
					</goals>
					<configuration>
						<transformers>
							<transformer
								implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
								<resource>META-INF/spring.handlers</resource>
							</transformer>
							<transformer
								implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
								<resource>META-INF/spring.schemas</resource>
							</transformer>
						</transformers>
						<filters>
							<filter>
								<artifact>*:*</artifact>
								<excludes>
									<exclude>META-INF/*.SF</exclude>
									<exclude>META-INF/*.DSA</exclude>
									<exclude>META-INF/*.RSA</exclude>
								</excludes>
							</filter>
						</filters>
					</configuration>
				</execution>
			</executions>
		</plugin>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-resources-plugin</artifactId>
			<version>2.6</version>
			<configuration>
				<encoding>UTF-8</encoding>
			</configuration>
		</plugin>
	</plugins>
</build>

使用本POM配置,打包、运行均成功没有出错。

参考链接:

官方:http://maven.apache.org/plugins/maven-shade-plugin/examples/resource-transformers.html

安全问题:http://zhentao-li.blogspot.com/2012/06/maven-shade-plugin-invalid-signature.html

Context问题:http://robert-reiz.com/2011/11/14/832/

 

本文地址:http://crazyant.net/1886.html,转载请注明来源

 

Java线程死亡的几种情况

Java线程会议如下三种方式结束,结束后就处于死亡状态

1、run()或者call()方法执行完成,线程正常结束;

2、线程抛出一个未捕获的Exception或Error;

3、直接调用该线程的stop()方法来结束该线程;

 

注意:当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来后,它就拥有和主线程相同的地位,不会受到主线程结束的影响。

为了测试某个线程是否已经死亡,可以调用线程对象的isAlive()方法,当线程处于就绪、运行、阻塞三种状态时,该方法将返回true;当线程处于新建、死亡两种状态时,该方法就返回false。

如下对线程死亡情况的1和2进行测试。主线程的代码如下:

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new RunTask());
        t.start();
        
        while (true) {
            Thread.sleep(1000);
            System.out.println("主线程:子线程状态为" + t.isAlive());
        }
    }
}

测试1:线程正常结束后,isAlive()返回False

编写线程正常结束的线程执行代码:

public class RunTask implements Runnable {
    
    @Override
    public void run() {
        for (int idx = 1; idx <= 10; idx++) {
            System.out.println("子线程:我还活着" + idx);
            
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

两个线程的输出结果如下所示,显示子线程正常执行结束后,使用Thread.isAlive()就返回False了。

主线程:子线程状态为true
主线程:子线程状态为true
子线程:我还活着8
主线程:子线程状态为true
主线程:子线程状态为true
主线程:子线程状态为true
子线程:我还活着9
主线程:子线程状态为true
主线程:子线程状态为true
主线程:子线程状态为true
子线程:我还活着10
主线程:子线程状态为true
主线程:子线程状态为true
主线程:子线程状态为true
主线程:子线程状态为false
主线程:子线程状态为false
主线程:子线程状态为false
主线程:子线程状态为false
主线程:子线程状态为false

测试2:子线程抛出异常之后,线程的isAlive()返回False

修改子线程的代码,加入异常抛出:

public class RunTask implements Runnable {
    
    @Override
    public void run() {
        for (int idx = 1; idx <= 10; idx++) {
            System.out.println("子线程:我还活着" + idx);
            
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            if (idx == 5) {
                throw new RuntimeException("i am die");
            }
        }
    }
}

再次执行,观察输出:

主线程:子线程状态为true
主线程:子线程状态为true
主线程:子线程状态为true
子线程:我还活着4
主线程:子线程状态为true
主线程:子线程状态为true
主线程:子线程状态为true
子线程:我还活着5
主线程:子线程状态为true
主线程:子线程状态为true
主线程:子线程状态为true
Exception in thread "Thread-0" java.lang.RuntimeException: i am die
	at RunTask.run(RunTask.java:15)
	at java.lang.Thread.run(Thread.java:662)
主线程:子线程状态为false
主线程:子线程状态为false
主线程:子线程状态为false

可以看到,抛出异常后,子线程直接终止,变成了Flase状态;

 

总结:线程正常结束后或者线程抛出了未捕获的异常,线程变成死亡状态,使用isAlive()函数返回False。

本文地址:http://crazyant.net/1861.html,转载请注明来源

通过JVM堆栈分析出现大量线程的原因

最近收到线上Tomcat线程数目超出的报警,于是想要分析下问题的原因:

首先进入线上,使用ps -aux命令,查看jvm进程,可以得到运行tomcat的jdk的地址:

/home/work/app/.jdk/bin/java

于是就知道了jdk的jstack、jps等命令的目录,然后找到jvm进程

/home/work/app/.jdk/bin/jps
29145 Jps
208 Bootstrap

得到了jvm的tomcat进程是208;

把堆栈导出,下载到本地:

jstack -l 208 > log.txt

下载后,发现线程堆栈中,有大量的这样的日志:

"pool-103-thread-1" prio=10 tid=0x00007f038001e000 nid=0x759d waiting on condition [0x00007f022e5e4000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x00000000912fab28> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:156)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1987)
	at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:399)
	at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:947)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:907)
	at java.lang.Thread.run(Thread.java:662)

"pool-102-thread-1" prio=10 tid=0x00007f0380011000 nid=0x71ed waiting on condition [0x00007f022e6e5000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x00000000912fa170> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:156)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1987)
	at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:399)
	at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:947)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:907)
	at java.lang.Thread.run(Thread.java:662)

可以看到,线程处于WAITING状态,阻塞在试图从任务队列中取任务(LinkedBlockingQueue.take),这个任务队列指的是ThreadPoolExecutor的线程池启动的线程任务队列;

也就是说,这些线程都是空闲状态,在等着任务的到来呢!

补充下LinkedBlockingQueue的知识:

并发库中的BlockingQueue是一个比较好玩的类,顾名思义,就是阻塞队列。该类主要提供了两个方法put()和take(),前者将一个对象放到队列尾部,如果队列已经满了,就等待直到有空闲节点;后者从head取一个对象,如果没有对象,就等待直到有可取的对象。 

定位到问题就简单了,查找代码,发现有个位置启动了线程池提交了任务,但是任务执行完返回后,线程池没有关闭导致的;

问题总结:

1、使用ExecutorService提交的线程任务,也要记得关闭;

2、启动新线程的时候,最好给线程起个名字,这样线程堆栈的问题排查更加容易;

 

文章地址:http://crazyant.net/1858.html,转载请注明来源

想要加悲观锁可是数据行还不存在怎么办?

两个并发事务想要对同一个KEY的数据进行更新,但是如果这个KEY的数据行还不存在的话,那么select .. for update当然不能锁住这行记录,想当然的想到,可不可以先insert一下,然后在悲观锁呢?

那么引入了一个新的问题,如果两个并发事务同时insert的话,就会插入重复的数据,如果insert的unique key重复的话,第二个线程会报错的,有没有更优雅的方法?

答案是MySQL innodb的INSERT … ON DUPLICATE KEY UPDATE语法;

用法见官网文档:https://dev.mysql.com/doc/refman/5.7/en/insert-on-duplicate.html

If you specify ON DUPLICATE KEY UPDATE, and a row is inserted that would cause a duplicate value in a UNIQUEindex or PRIMARY KEY, MySQL performs an UPDATE of the old row. For example, if column a is declared as UNIQUEand contains the value 1, the following two statements have similar effect:

INSERT INTO table (a,b,c) VALUES (1,2,3)
  ON DUPLICATE KEY UPDATE c=c+1;

UPDATE table SET c=c+1 WHERE a=1;

使用ON DUPLICATE KEY UPDATE语法,第二条insert语句,会自动变成Update语句,而不会导致重复插入数据的BUG;

做个简单的测试:

CREATE TABLE `tcc` (
  `idx` INT(11),
  `typeid` INT(11),
  `value1` INT(11),
  `value2` INT(11),
  UNIQUE KEY(idx, typeid)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

-- 第一条SQL的执行,会新增一条数据,本SQL多次执行,效果相同,因为后续的操作,变成了UPDATE
INSERT INTO tcc (idx, typeid, value1) 
VALUES (1,2,3) ON DUPLICATE KEY UPDATE value1=3;

-- 第二条SQL直接进行UPDATE,把新列的值update进去
INSERT INTO tcc (idx, typeid, value2) 
VALUES (1,2,4) ON DUPLICATE KEY UPDATE value2=4;

产出结果只有一条:

“idx” “typeid” “value1” “value2”
“1” “2” “3” “4”

有了这个利器,那么代码中就可以直接insert.. on duplicate key update,这个SQL的执行能保证数据库中会存在记录,然后加上悲观锁,来保证不同的事务不会出现更新冲突情况;

本文地址:http://crazyant.net/1835.html

Java堆溢出OutOfMemoryError之代码实例和原因分析

本文演示了编写代码使得出现”java.lang.OutOfMemoryError: Java heap space”异常,分析GC日志得出OOM的原因,同时对堆转储文件进行分析,以查看把Heap塞满的罪魁祸首;

实例代码

这段代码来自《深入理解Java虚拟机-JVM高级特性与最佳实践》一书:

package jvmtest;

import java.util.ArrayList;
import java.util.List;

/**
 * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * 
 * @author zzm
 */
public class HeapOOM {
    
    static class OOMObject {
    }
    
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        
        while (true) {
            list.add(new OOMObject());
        }
    }
}

在eclipse运行该代码时,需要设置堆size的最小值和最大值,同时使用-XX:+PrintGCDetails参数开启GC日志打印,使用-XX:+HeapDumpOnOutOfMemoryError参数当OOM时转储堆数据

-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+HeapDumpOnOutOfMemoryError

在eclipse中设置vm arguments参数为了测试OOM

然后点运行,会出现以下的GC日志、异常、堆信息、转储信息

[GC[DefNew: 7640K->1024K(9216K), 0.0164901 secs] 7640K->4593K(19456K), 0.0166075 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[GC[DefNew: 9216K->1024K(9216K), 0.0190825 secs] 12785K->10510K(19456K), 0.0191593 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[GC[DefNew: 9216K->9216K(9216K), 0.0000369 secs][Tenured: 9486K->5704K(10240K), 0.0520898 secs] 18702K->14129K(19456K), [Perm : 148K->148K(12288K)], 0.0522306 secs] [Times: user=0.06 sys=0.00, real=0.05 secs] 
[Full GC[Tenured: 5704K->5704K(10240K), 0.0435436 secs] 14616K->14616K(19456K), [Perm : 148K->148K(12288K)], 0.0436766 secs] [Times: user=0.03 sys=0.00, real=0.04 secs] 
[Full GC[Tenured: 5704K->5695K(10240K), 0.0499650 secs] 14616K->14606K(19456K), [Perm : 148K->147K(12288K)], 0.0500832 secs] [Times: user=0.06 sys=0.00, real=0.05 secs] 
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid12428.hprof ...
Heap dump file created [29715028 bytes in 0.335 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:2245)
	at java.util.Arrays.copyOf(Arrays.java:2219)
	at java.util.ArrayList.grow(ArrayList.java:242)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208)
	at java.util.ArrayList.add(ArrayList.java:440)
	at jvmtest.HeapOOM.main(HeapOOM.java:20)
Heap
 def new generation   total 9216K, used 8921K [0x33640000, 0x34040000, 0x34040000)
  eden space 8192K, 100% used [0x33640000, 0x33e40000, 0x33e40000)
  from space 1024K,  71% used [0x33e40000, 0x33ef6600, 0x33f40000)
  to   space 1024K,   0% used [0x33f40000, 0x33f40000, 0x34040000)
 tenured generation   total 10240K, used 5695K [0x34040000, 0x34a40000, 0x34a40000)
   the space 10240K,  55% used [0x34040000, 0x345cfc28, 0x345cfe00, 0x34a40000)
 compacting perm gen  total 12288K, used 150K [0x34a40000, 0x35640000, 0x38a40000)
   the space 12288K,   1% used [0x34a40000, 0x34a658c8, 0x34a65a00, 0x35640000)
    ro space 10240K,  44% used [0x38a40000, 0x38eb73f0, 0x38eb7400, 0x39440000)
    rw space 12288K,  52% used [0x39440000, 0x39a8dd28, 0x39a8de00, 0x3a040000)

GC日志分析

从图中可以看出,发生了3次GC和2次FULL GC,当两次FULL GC完成后,仍然发现没有空间,于是抛出了OOM异常;

首先贴一下GC和FULL GC的日志格式:

young gc log format

full gc log format

还有jvm heap的内存结构

jvm-memory-model

在正常运行状况下:

  1. 新的对象都在eden上创建
  2. 如果eden的大小达到了阈值,就会触发GC,运行“标记-复制”算法,将Eden中有效的对象复制到Survivor中,并清除eden中无效的对象
  3. 如果eden的大小再次达到阈值,就会触发GC,将对象从eden对象复制到survivor,如果survivor中的对象达到了年龄限制,就会复制到old区;

如果young区向old区复制对象的时候,发现old区的空间无法满足,就会触发FULL GC,如果FULL GC之后,仍然无法承载Young区要晋升的对象大小,那么就会抛出OOM;

从Heap开头的日志可以看到eden区已经占用了100%,from survivor区也占用了71%无法承载来自eden的对象,eden+from survivor区对象之和为8192K+1024K*71%=8919.04K,但是old区只剩下10240*(1-55%)=的空间4608K的空间;新的对象无法在100%占用率的eden区创建,eden+survivor的对象又不能复制到old区,所以OOM;

注意:本文的代码很特殊,每个HeapOOM对象都没有被释放,正常情况下,eden很多无用对象是会被清除掉的,晋升到OLD的大小也不等于eden+survivor的大小之和;

堆转储文件分析

由于加上了-XX:+HeapDumpOnOutOfMemoryError参数,所以OOM时,自动生成了一个java_pid12464.hprof的堆转储文件,给eclipse安装Memory Analyzer插件后,直接可以用该插件打开;

打开后如下图所示

oom-memory-analyzer

可以看到main函数线程占用了13.9MB几乎所有的Heap空间,在饼图上右键 >  list objects > with outgoing references,可以查看该部分包含了那些对象:

oom-object-list

打开后发现,一个Obejct数组中包含了1215488个元素,每个元素都是HeapOOM对象,每个元素(引用类型)为8byte,这就是让堆OOM的罪魁祸首;

结论

  • 使用JVM参数-XX:+PrintGCDetails可以打印GC日志
  • 使用JVM参数-XX:+HeapDumpOnOutOfMemoryError可以在OOM时打印堆转储文件
  • 使用eclipse插件Memory Analyzer可以分析对转储文件
  • OOM的原因,是Old区在FULL GC之后的剩余空间,仍然无法承载Young区要晋升的对象大小

本文地址:http://crazyant.net/1810.html,转载请注明

使用javap命令或者eclipse的Bytecode visualizer插件阅读java字节码文件

阅读java的class文件,最常用的方法是jdk自带的javap命令,但是在eclipse有Bytecode visualizer插件,也可以很好地实现阅读;

直接阅读class文件,能够了解一下代码如何执行的内幕,以下面这个代码为例

package test;

import java.util.List;

public class Test {
	public void test() {
		String a = "x" + "y" + 1;
		String b = "xy1";
		System.out.println(a == b);
	}

	public static void main(String[] args) {
		Test t = new Test();

		t.test();
	}
}

test()方法很多人都知道返回true,但是为什么会这样呢?很多人可能说不清楚。我们分别用两种方法阅读一下它们的字节码;

该文件首先需要被编译,得到了Test.class文件

1、使用javap命令阅读

进入Test.class目录,然后使用javap -v Test.class > out将字节码文件读取后存入out文件,然后用notepad++打开out文件:

test()函数的样子:

bytecode-20150705102150

从字节码可以看到,编译器已经自动的将变量a的值合并在一起,成为了xy1,因此相当于xy1两个字符串的比较,两者都处于常量区,因此相等;

2、使用eclipse的Bytecode visualizer插件阅读

安装好插件之后,首先将其配置一下,以显示最大化的字节码信息

bytecode-visualizer-config

然后打开eclipse的Navigator窗口,找到编译好的Test.class,右键,用字节码方式打开,就会看到更易读的字节码

bytecode-visualizer-show

同时在编辑器的右侧,能够自动查看该段代码的流程图,非常方便;

 

使用这两种方式,就能够查看代码编译后的内幕,对一些奇怪的语法现象,就能够自己找出为什么了

但是在测试的过程中,自己写了个print(List<String> strs)的方法,使用Javap命令和Byte visualizer两者,输出的字节码中,前者带着泛型信息,后者进行了泛型擦除,两者不同,很是奇怪;

Java怎样单测void类型的方法?

Java的Sevice层会有很多void类型的方法,比如save*、update*,这类方法只是做一些更新,不会有返回值,其单测不能根据方法的返回值来编写,只能采用特殊方法;

本方法环境:Mockito、testng

被测试的方法:

@Override
    public void updateRuleName(Long ruleId, String newRuleName, Long ucId) {
        Assert.notNull(ruleId, "规则ID不能为Null");
        Assert.notNull(newRuleName, "规则名称不能为Null");
        Assert.notNull(ucId, "操作人的UCID不能为Null");
        
        String cleanNewRuleName = StringUtils.trim(newRuleName);
        if (StringUtils.isBlank(cleanNewRuleName)) {
            throw new IllegalArgumentException("新的规则名称不能为空");
        }
        
        // 查询规则对象
        Rule rule = queryRuleById(ruleId);
        if (null == rule) {
            throw new IllegalDataException("没有查到该规则");
        }
        
        rule.setRuleId(ruleId);
        rule.setRuleName(cleanNewRuleName);
        rule.setUpdateUcid(ucId);
        rule.setUpdateTime(new Date());
        
        ruleDao.updateSelective(rule);
    }

测试的方法:

 @Test
    public void testUpdateRuleName() {
        Long ruleId = 1L;
        String newRuleName = "newRuleName";
        Long ucId = 123L;
        
        List<Rule> rules = new ArrayList<Rule>();
        Rule rule = new Rule();
        rule.setRuleStatus((byte) DBValueSetting.RULE_STATUS_TAKE_EFFECT);
        rules.add(rule);
        
        // 查询规则对象
        Map<String, Object> params = new HashMap<String, Object>();
        params.put("ruleId", ruleId);
        Mockito.when(ruleDao.queryRulesByCondition(params)).thenReturn(rules);
        
        Mockito.doAnswer(new Answer<Object>() {
            public Object answer(InvocationOnMock invocation) {
                // 断点2:这里随后执行
                Rule rule = (Rule) invocation.getArguments()[0];
                Assert.assertTrue(rule.getRuleName().equals("newRuleName"));
                return null;
            }
        }).when(ruleDao).updateSelective(Mockito.any(Rule.class));
        
        // 断点1:先执行到这里
        ruleService.updateRuleName(ruleId, newRuleName, ucId);
    }

如注释所示,如果加了两个断点的话,执行的过程中,会先执行最后的调用行,端点1执行的过程中,会执行到端点2的stub,这时候在断点2可以获取到方法执行的入参,对入参进行Assert校验,即可实现目的;

new Anwer是个接口,其中只有一个方法,用于设置方法调用的代理执行入口

public interface Answer<T> {
    /**
     * @param invocation the invocation on the mock.
     *
     * @return the value to be returned
     *
     * @throws Throwable the throwable to be thrown
     */
    T answer(InvocationOnMock invocation) throws Throwable;
}

当代码执行到“ruleDao.updateSelective(rule);”的时候,会触发针对mock对象调用的拦截器,在拦截器中,会创建一个动态代理,动态代理的invocation就是new Answer中覆盖的方法;

使用拦截、代理两种方法,实现了对mock对象方法的入参、出参的设定和获取,使用这种方式,就可以校验VOID方法内部的执行类调用的情况;

 

 

《Spring in action》3rd中SpringPizza项目的运行方法

《Spring in action》3rd中SpringPizza项目和其他的不同,使用gradle编译的,尝试了好久都没能run起来,最后按照一篇文章的做法,将其转换成maven项目,然后就执行成功了。

1、下载代码

下载地址:http://www.manning.com/walls4/sia3-code.zip,解压代码

2、在\sia3-code\SpringPizza目录下建立pom.xml文件,把项目变成maven编译

pom.xml文件内容如下:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.yogesh</groupId>
	<artifactId>SpringPizza</artifactId>
	<packaging>war</packaging>
	<version>1.0-SNAPSHOT</version>
	<name>spring-web-flow Maven Webapp</name>
	<url>http://maven.apache.org</url>
	<properties>
		<spring.version>3.0.5.RELEASE</spring.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.webflow</groupId>
			<artifactId>spring-webflow</artifactId>
			<version>2.3.2.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.webflow</groupId>
			<artifactId>spring-binding</artifactId>
			<version>2.3.2.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>jstl</groupId>
			<artifactId>jstl</artifactId>
			<version>1.2</version>
		</dependency>
		<dependency>
			<groupId>commons-lang</groupId>
			<artifactId>commons-lang</artifactId>
			<version>2.6</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-beans</artifactId>
			<version>3.2.4.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.tuckey</groupId>
			<artifactId>urlrewritefilter</artifactId>
			<version>3.1.0</version>
		</dependency>
		<dependency>
			<groupId>log4j</groupId>
			<artifactId>log4j</artifactId>
			<version>1.2.7</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-aspects</artifactId>
			<version>2.0-m2</version>
		</dependency>
	</dependencies>

	<build>
		<finalName>SpringPizza</finalName>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.3.2</version>
				<configuration>
					<source>1.6</source>
					<target>1.6</target>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

3、使用maven项目的方式,导入SpringPizza

1

 

导入后发现/SpringPizza/src/test/java/com/springinaction/pizza/flow/PizzaFlowTest.java单测报错,半天没搞定,先删除,对运行项目没有影响;

4、把项目转换成web形式并且添加maven依赖到编译路径

4

5
5、编译安装项目

3

6、把编译的项目添加到Server中

6

 

7、启动tomcat,访问首页

地址是:http://localhost:8080/SpringPizza/pizza

7

这样就可以结合代码和执行,来仔细研究web flow的代码了。

《Spring in action》3rd真是本好书,带我进入了深入理解Spring的大门,感谢作者。

 

 

Java怎样创建两个KEY(key-pair)的MAP

就像在XY坐标系中,一个X刻度、一个Y刻度,会对应图上的一个点,即pair(x, y) – > point,那么就有创建一个点,或者根据(x, y)来寻求一个点的需求,如果用Python的语法表达,是这个样子:

dict((x, y) : point)

然而在JAVA中却变得不容易,骨钩了一下,有这么一些方法:

使用嵌套MAP

Map<Integer, Map<Integer, V>> map = //...
// ...

map.get(2).get(5);

这种方法将水平的KEY切换成了垂直KEY,我们要找寻XY坐标系中(x, y)对应的点,其实可以先找x点的那条垂直线,然后找出这条垂直线上y高度的那个点。

但是要取得这个双KEY的VALUE,得有2次GET,实例代码没有做第一次GET的NULL判断,有隐患!并且感觉不是很直观。 继续阅读Java怎样创建两个KEY(key-pair)的MAP