2010年9月27日
在使用二这编文章中提到,可以以汇总形式查看日志,也可以通过图表来查看。Perf4j跟log4j集成后,其实也可以以图表的形式来查看性能状况。
我们这篇文章还是使用前一篇文章中提到log4j.xml的配置,其他都一样,只是在配置中加入了图表的配置:
<!-- 生成firstBlock,secondBlock的平均值的图表 -->
<appender name="graphExecutionTimes"
class="org.perf4j.log4j.GraphingStatisticsAppender">
<!-- GraphType:Mean(平均值), Min(最小值), Max(最大值), TPS(每秒事务数) -->
<param name="GraphType" value="Mean"/>
<param name="TagNamesToGraph" value="firstBlock,secondBlock"/>
<appender-ref ref="graphsFileAppender"/>
</appender>
<!-- 生成firstBlock,secondBlock的tps的图表 -->
<appender name="graphExecutionTPS"
class="org.perf4j.log4j.GraphingStatisticsAppender">
<param name="GraphType" value="TPS"/>
<param name="TagNamesToGraph" value="firstBlock,secondBlock"/>
<appender-ref ref="graphsFileAppender"/>
</appender>
<!-- 记录图表生成url的log文件 -->
<appender name="graphsFileAppender" class="org.apache.log4j.FileAppender">
<param name="File" value="/home/perfGraphs.log"/>
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%m%n"/>
</layout>
</appender>
另外还需要改一个地方,就是CoalescingStatistics的配置:
<appender name="CoalescingStatistics"
class="org.perf4j.log4j.AsyncCoalescingStatisticsAppender">
<!--
TimeSlice配置多少时间间隔去做一次汇总写入文件中
默认值是 30000 ms
-->
<param name="TimeSlice" value="10000"/>
<appender-ref ref="fileAppender"/>
<appender-ref ref="graphExecutionTimes"/>
<appender-ref ref="graphExecutionTPS"/>
</appender>
黄色那段配置的意思就是把日志写入到图表日志去。
运行代码Perf4JAppenderExample,我们在perfGraphs.log文件中生成了图表的url:
http://chart.apis.google.com/chart?cht=lxy&chtt=Mean&chs=750x400&chxt=x,x,y&chd=t:0.0,100.0|45.2,78.1|0.0,100.0|98.1,100.0&chco=ff0000,00ff00&chm=d,ff0000,0,-1,5.0|d,00ff00,1,-1,5.0&chdl=firstBlock|secondBlock&chxr=2,0,828.6&chxl=0:|13:23:50|13:24:00|1:|Time&chxp=0,0.0,100.0|1,50&chg=50.0,10
http://chart.apis.google.com/chart?cht=lxy&chtt=TPS&chs=750x400&chxt=x,x,y&chd=t:0.0,100.0|100.0,100.0|0.0,100.0|100.0,100.0&chco=ff0000,00ff00&chm=d,ff0000,0,-1,5.0|d,00ff00,1,-1,5.0&chdl=firstBlock|secondBlock&chxr=2,0,0.5&chxl=0:|13:23:50|13:24:00|1:|Time&chxp=0,0.0,100.0|1,50&chg=50.0,10
大家可以把url放到浏览器访问下。
上面这种方式呢,需要自己登录到服务器上,找到log文件,在放到浏览器中查看,总的过程还是比较麻烦。如果大家需要监控的工程是一个web工程的话,那就更方便了,直接配置一个servlet来查看。Web.xml的配置如下:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>perf4j</servlet-name>
<servlet-class>org.perf4j.log4j.servlet.GraphingServlet</servlet-class>
<!-- graphExecutionTimes和graphExecutionTPS就是我们在log4j中配置的名称 -->
<init-param>
<param-name>graphNames</param-name>
<param-value>graphExecutionTimes,graphExecutionTPS</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>perf4j</servlet-name>
<url-pattern>/perf4j</url-pattern>
</servlet-mapping>
</web-app>
大家可以打包工程,并放到web服务器下启动,然后访问下/perf4j这个uri。
Maven有一个jetty插件,可以方便启动web工程,只要大家在pom.xml文件中加入如下配置:
<plugins>
<!-- jetty插件, 设定端口与context path-->
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
</plugin>
</plugins>
在控制台中输入:mvn jetty:run,即可。省去了打包发布,很省心喔。
第一次用http://localhost:8080/perf4j访问查看图表的时候没有生成任何东西,那是因为内存中没有收集到最新的性能数据。所以我在index.jsp里调用下以便产生性能数据。然后重新访问,这个时候就有图表生成了。
下载工程
Perf4j最主要的一个好处就是可以跟log4j或者logback来性能分析和监控线上运行的程序。集成的方式主要是:自定义log4j的appenders通过标准的配置加入到log4j中去(后面会有配置的例子)。有一个要注意的地方就是需要使用log4j的1.2.14版本或者更高版本。由于我一般都是使用log4j,所以对于logback的集成我就不描述了,我觉得应该差不多的。
Perf4j最重要的appender就是AsyncCoalescingStatisticsAppender,它会把一段时间内StopWatch的信息汇总到一个独立的GroupedTimingStatistics日志信息,然后把这个独立的信息传给下游的appenders,比如fileappenders,这样就可以写到文件中去了。也可以传给per4j的其他自定义appenders。
接下来我们看一个log4j.xml的例子,有一个限制,如果要使用AsyncCoalescingStatisticsAppender就只能使用xml文件而不能使用properties文件。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration debug="false" xmlns:log4j="http://jakarta.apache.org/log4j/">
<!--
配置控制台输出
-->
<appender name="console" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%-5p %c{1} - %m%n"/>
</layout>
</appender>
<!-- Perf4J appenders -->
<!--
AsyncCoalescingStatisticsAppender收集StopWatch的日志信息并传送到下游的文件appenders。
-->
<appender name="CoalescingStatistics"
class="org.perf4j.log4j.AsyncCoalescingStatisticsAppender">
<!--
TimeSlice配置多少时间间隔去做一次汇总写入文件中
默认值是 30000 ms
-->
<param name="TimeSlice" value="10000"/>
<appender-ref ref="fileAppender"/>
</appender>
<!-- 把汇总的perf4j的日志信息写到perfStats.log文件中去 -->
<appender name="fileAppender" class="org.apache.log4j.FileAppender">
<param name="File" value="/home/perfStats.log"/>
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%m%n"/>
</layout>
</appender>
<!-- Loggers -->
<!--
配置perf4j logger
Additivity设置成false主要因为是不想让代码运行时间的日志输出给上游appenders,即不要在控制台输出。
-->
<logger name="org.perf4j.TimingLogger" additivity="false">
<level value="INFO"/>
<appender-ref ref="CoalescingStatistics"/>
</logger>
<!--
Root logger打印所有日志,但不包含perf4j的信息。原因是在TimingLogger配置中设置了additivity为false
-->
<root>
<level value="INFO"/>
<appender-ref ref="console"/>
</root>
</log4j:configuration>
黄色背景是perf4j的配置信息。其他都是log4j的基本配置。下面是测试perf4j与log4j集成的代码。
package com.baowu.perf4j;
import org.apache.log4j.Logger;
import org.perf4j.StopWatch;
import org.perf4j.log4j.Log4JStopWatch;
public class Perf4JAppenderExample {
public static void main (String[] args) throws Exception {
Logger rootLogger = Logger.getRootLogger();
for (int i = 0; i < 10; i++) {
// Log4JStopWatch默认使用org.perf4j.TimingLogger这个类
StopWatch stopWatch = new Log4JStopWatch();
//模拟代码运行时间
Thread.sleep((long) (Math.random() * 1000L));
//打印到控制台
rootLogger.info("Normal logging messages only go to the console");
stopWatch.lap("firstBlock");
Thread.sleep((long) (Math.random() * 2000L));
stopWatch.stop("secondBlock");
}
}
}
运行代码。
控制台输出:
INFO root - Normal logging messages only go to the console
INFO root - Normal logging messages only go to the console
INFO root - Normal logging messages only go to the console
INFO root - Normal logging messages only go to the console
INFO root - Normal logging messages only go to the console
INFO root - Normal logging messages only go to the console
INFO root - Normal logging messages only go to the console
INFO root - Normal logging messages only go to the console
INFO root - Normal logging messages only go to the console
INFO root - Normal logging messages only go to the console
文件输出:
输出格式也可以改成CSV格式。配置:
<appender name="fileAppender" class="org.apache.log4j.FileAppender">
<param name="File" value="/home/perfStats.log"/>
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%m%n"/>
</layout>
</appender>
org.apache.log4j.PatternLayout改成org.perf4j.log4j.StatisticsCsvLayout即可。
具体的参数请查看api。
下载工程
Perf4j使用一主要演示了性能监控的日志直接打印在标准输出流。那么使用二呢则主要来演示怎么来分析打印出来的日志文件。
由于我们还没有跟log4j集成,日志文件打印在标准输出流,我们需要把标准输出流重定向到times.log文件中。重定向有两种方式:直接copy到文件中,或者在eclipse里指定下输出文件。我主要是用eclipse指定输出文件。
然后运行代码(Perf4j使用一的Example.java),控制台会在第一句话中打出[Console output redirected to file:E:\yangpingyu\work\times.log],这样运行的结果会同时打印在文件中和标准输出中。
有了times.log,我们就可以对日志文件进行分析,以找出有问题的代码。
分析日志命令:
E:\yangpingyu\work>java -jar perf4j-0.9.16.jar times.log
以csv的格式来查看结果,命令如下:
java -jar perf4j-0.9.16.jar -f csv times.log
以上都是以文本的格式进行输出,但文本没有图表更具有表达力。所以把结果以图表形式输出是必不可少,幸好perf4j也支持,命令如下:
java -jar perf4j-0.9.16.jar --graph perfGraphs.html times.log
执行命令后,在控制台输出相应的统计信息,相应的在磁盘上也生成了一个html,html里包含平均值图表和tps图表。
<html>
<head><title>Perf4J Performance Graphs</title></head>
<body>
<br/><br/><img src="http://chart.apis.google.com/chart?cht=lxy&chtt=Mean&chs=750x400&chxt=x,x,y&chd=t:0.0,50.0,100.0|56.3,60.1,6.0|0.0,50.0|88.5,94.5|50.0,100.0|43.2,7.2|0.0,50.0,100.0|71.8,57.4,8.0|0.0,50.0,100.0|100.0,61.2,59.6|0.0,50.0,100.0|63.9,62.0,18.7|0.0,50.0,100.0|34.4,72.1,30.1&chco=ff0000,00ff00,0000ff,00ffff,ff00ff,ffff00,000000&chm=d,ff0000,0,-1,5.0|d,00ff00,1,-1,5.0|d,0000ff,2,-1,5.0|d,00ffff,3,-1,5.0|d,ff00ff,4,-1,5.0|d,ffff00,5,-1,5.0|d,000000,6,-1,5.0&chdl=codeBlock1|codeBlock2.failure|codeBlock2.success|codeBlock3|codeBlock4|codeBlock5|codeBlock6&chxr=2,0,748.5&chxl=0:|18:12:00|18:12:30|18:13:00|1:|Time&chxp=0,0.0,50.0,100.0|1,50&chg=50.0,10"/>
<br/><br/><img src="http://chart.apis.google.com/chart?cht=lxy&chtt=TPS&chs=750x400&chxt=x,x,y&chd=t:0.0,50.0,100.0|36.4,90.9,9.1|0.0,50.0|27.3,63.6|50.0,100.0|36.4,9.1|0.0,50.0,100.0|36.4,90.9,9.1|0.0,50.0,100.0|36.4,90.9,9.1|0.0,50.0,100.0|36.4,90.9,9.1|0.0,50.0,100.0|27.3,100.0,9.1&chco=ff0000,00ff00,0000ff,00ffff,ff00ff,ffff00,000000&chm=d,ff0000,0,-1,5.0|d,00ff00,1,-1,5.0|d,0000ff,2,-1,5.0|d,00ffff,3,-1,5.0|d,ff00ff,4,-1,5.0|d,ffff00,5,-1,5.0|d,000000,6,-1,5.0&chdl=codeBlock1|codeBlock2.failure|codeBlock2.success|codeBlock3|codeBlock4|codeBlock5|codeBlock6&chxr=2,0,0.4&chxl=0:|18:12:00|18:12:30|18:13:00|1:|Time&chxp=0,0.0,50.0,100.0|1,50&chg=50.0,10"/>
</body></html>
以上是html的内容,里面最重要的信息就是两个img标签,里面具体的图片是google chart api生成。可以打开html直接查看图表。
如果想要看更详细的参数,可以使用—help来查看。java -jar perf4j-0.9.16.jar –help。
本文主要记录下自己在日常工作中感觉比较好用的一些小工具。目前只有几个,不过遇到好的会不断更新上来。
EmEditor:文本编辑器。
onenote:主要用来记录工作列表和一些计划等。
google chart api:可以用来制作图表,很方便。性能测试的结果数据做对比图。
smartArt:也是图表制作工具,已经集成到2007office中了。
awk:linux文本处理工具。类似于脚本。牛逼的程序员应该要掌握的一个工具。
add by 2012-06-19
http://cn.edrawsoft.com/download.php :画图很漂亮的工具。
待更新。。。
如果大家使用的是maven工程,那么现在pom文件中加入perf4j的依赖。
<dependency>
<groupId>org.perf4j</groupId>
<artifactId>perf4j</artifactId>
<version>0.9.16</version>
<scope>compile</scope>
</dependency>
如果用的是普通工程,那么直接下载jar包放入lib目录下即可。
例子:
package com.baowu.per4j;
import org.perf4j.LoggingStopWatch;
import org.perf4j.StopWatch;
public class Example1 {
public static void main(String[] args) throws InterruptedException{
method1();
method2();
method3();
}
/**
* 监控一处代码示例
* @throws InterruptedException
*/
private static void method1() throws InterruptedException{
//创建一个监控对象,这里使用LoggingStopWatch,它是把结果直接输出到控制台。我们也可以
//使用StopWatch的其他子类,比如:Log4JStopWatch,CommonsLogStopWatch。不过这些子类需
//要工程使用日志框架
StopWatch stopWatch = new LoggingStopWatch("codeBlock1");
//这里就是一些需要监控的代码,我们命名为codeBlock1
//使用线程休眠是为了模拟代码执行时间
Thread.sleep((long)(Math.random() * 1000L));
//停止计算代码性能
stopWatch.stop();
}
/**
* 一个方法多出代码监控
* @throws InterruptedException
*/
private static void method2() throws InterruptedException{
StopWatch stopWatch = new LoggingStopWatch();
Thread.sleep((long)(Math.random() * 1000L));
stopWatch.lap("codeBlock3");
Thread.sleep((long)(Math.random() * 1000L));
stopWatch.lap("codeBlock4");
Thread.sleep((long)(Math.random() * 1000L));
stopWatch.lap("codeBlock5");
Thread.sleep((long)(Math.random() * 1000L));
stopWatch.stop("codeBlock6");
}
/**
* stop方法可以加入一些说明信息
*/
private static void method3(){
StopWatch stopWatch = new LoggingStopWatch();
try {
// the code block being timed - this is just a dummy example
long sleepTime = (long)(Math.random() * 1000L);
Thread.sleep(sleepTime);
if (sleepTime > 500L) {
throw new Exception("Throwing exception");
}
stopWatch.stop("codeBlock2.success", "Sleep time was < 500 ms");
} catch (Exception e) {
stopWatch.stop("codeBlock2.failure", "Exception was: " + e);
}
}
}
运行结果:
start[1334457619937] time[355] tag[codeBlock1]
start[1334457620296] time[152] tag[codeBlock3]
start[1334457620453] time[138] tag[codeBlock4]
start[1334457620593] time[598] tag[codeBlock5]
start[1334457621187] time[700] tag[codeBlock6]
start[1334457621890] time[619] tag[codeBlock2.failure] message[Exception was: java.lang.Exception: Throwing exception]
Perf4j主要的用途是计量代码性能和分析性能数据。
为什么要使用这个工具呢?我们可以联想下最早期java开发者调试代码使用的方式,以前没有日志框架,那java开发就使用System.out.println()来输出自己想查看的变量。但是这样项目上线的话,就要去掉这些打印语句以减少性能影响。那万一在线上出问题了,调试哪里出问题就很麻烦,因为没有输出的日志可查。所以后来有人开发了日志框架,通过日志级别控制日志的输出。
类似的,如果没有perf4j,我们在查看代码运行时间的话可以用以下代码来实现:
long start = System.currentTimeMillis();
// execute the block of code to be timed
System.out.println("ms for block n was: " + (System.currentTimeMillis() - start));
这种方式有几个缺点:
1、 这种方式输出内容比较单一,就是代码总的运行时间。但是我们代码需要查看的性能指标有更多,比如总的平均值,最小值,最大值,tps等等。
2、 也许我们的代码在线上运行,我们想把这些值通过图表的形式展示出来。或者把这些内容通过jmx输出。
3、 另外,我们可能把perf4j跟log4j,slf4j等日志框架和日志门面系统整合起来。
基于以上这些问题,所以开源社区就出现了perf4j(人多力量大,社区的力量就是强大)。
Perf4j一些特性:
l 简单的停止查看机制来计算语句时间消耗输出。
l 命令行解析log文件产生汇总数据和图表。
l 简单的集成日志框架和门面框架。
l 自定义log4j和logback的appenders来产生数据和图表。
l 通过jmx查看性能指标,并根据阈值发送消息。
l Web工程可以通过servlet来输出性能指标。
l Perf4j可以和aop等切面框架整合起来输出性能指标。
l Perf4j是一个可扩展的架构。
公司最近严抓软件质量问题,我抽空了解了下提高代码质量的一些开源工具。其中一个就是findbugs。使用findbugs有很多方式,比如:安装eclipse findbugs插件,通过maven调用生成报告。今天主要演示下maven与findbugs集成。
第一步:下载maven,我使用的是maven3。把maven的命令加入PATH环境变量。
第二步:创建一个普通的maven java工程。命令如下:mvn archetype:maven-archetyp-quickstart –DgroupId=com.tianya –DartifactId=baowu。如果正常执行的话会生成如下结构的一个工程。
第三步:我们看下pom文件
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.tianya</groupId>
<artifactId>baowu</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<!--配置插件来源 -->
<pluginRepositories>
<pluginRepository>
<id>Codehaus repository</id>
<url>http://repository.codehaus.org/</url>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>findbugs-maven-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<!-- <configLocation>${basedir}/springside-findbugs.xml</configLocation> -->
<threshold>High</threshold>
<effort>Default</effort>
<findbugsXmlOutput>true</findbugsXmlOutput>
<!-- findbugs xml输出路径--> <findbugsXmlOutputDirectory>target/site</findbugsXmlOutputDirectory>
</configuration>
</plugin>
</plugins>
</build>
</project>
我来解释下xml配置:
l 配置插件下载地址
<!--配置插件来源 -->
<pluginRepositories>
<pluginRepository>
<id>Codehaus repository</id>
<url>http://repository.codehaus.org/</url>
</pluginRepository>
</pluginRepositories>
l 由于maven核心做的事情都是抽象的构建过程,很多实际的工作都是具体的插件来实现。所以很显然,maven以插件的方式集成findbugs。
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>findbugs-maven-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<!-- <configLocation>${basedir}/springside-findbugs.xml</configLocation> -->
<!-- findbugs xml输出--> <findbugsXmlOutput>true</findbugsXmlOutput>
<!-- findbugs xml输出路径--> <findbugsXmlOutputDirectory>target/site</findbugsXmlOutputDirectory>
</configuration>
</plugin>
l 大家注意到了findbugs插件里,我注释掉了一句话,其实这句话就是可以使用自己的fingbugs配置来做检查。我用的是springside的一个xml文件。
第四步:配置好相关文件之后,接下来就是执行相关命令了。
mvn compile findbugs:findbugs生成报告。报告生成的地址就是${项目根目录}/target/site。也可以通过mvn findbugs:gui gui界面查看findbugs的report。
Java语言与c语言有一个非常重要的区别就是:内存管理方式的不同,java语言内存管理不需要程序开发人员关注,而c语言的内存的请求和释放都是开发人员来处理。辩证的思维来看,不同内存管理实现方式有优点和缺点,所以语言应用的场景,效率会有很大不同。
Jvm运行时的数据区域主要有:程序计数器、虚拟机栈、本地方法栈、方法区和堆。其中程序计数器、虚拟机栈和本地方法栈是线程独享,而方法区和堆是所有线程共享。
ü 程序计数器:jvm每个线程都有一个程序计数器。在任一时刻都有一个线程的方法在运行,如果这个方法不是本地方法,那么程序计数器存放的就是正在执行的指令地址;如果是本地方法,那么程序计数器中存放的指定地址为undefined。
ü 虚拟机栈:当jvm创建一个线程的时候就会为线程分配一个虚拟机栈。主要用于存放方法的一些本地变量和部分结果,一般这里的大小都是固定,但不是绝对。一个方法的执行到完成就是栈的入栈和出栈。假设在某方法中定义了一个对象Object obj=new Object();其中obj是存放在栈上,而new Object()是在堆上分配。-Xss可以控制jvm虚拟机栈的大小。
ü 本地方法栈:大体跟虚拟机栈类似,不过是给本地方法使用的。虚拟机栈和本地方法栈在hotspot是没有分开实现的,而是统称为栈。
ü 方法区:主要存放静态变量,常量,类加载器加载的类等一些信息。
ü 堆:jvm绝大部分的对象分配都在堆上分配。-Xmn –Xmx是控制堆最小值和最大值,一般堆的大小在使用了超过mx设定的70%的时候,就会自动扩大到最大值,所以防止这种扩大和缩小我们设置成一样的值。
ClassCastException类型转换异常,是一个运行时异常。
非常常见就是不同类型之间的强制类型转换就会抛出ClassCastException异常。还有一种就是不同ClassLoader加载的相同的类型转换也会抛出ClassCastException。接下来我用代码来详细解释下。
1、 强制类型转换
public class ClassCastExceptionTest {
/**
* @param args
*/
public static void main(String[] args) {
Animal a1 = new Dog(); //1
Animal a2 = new Cat(); //2
Dog d1 = (Dog)a1; //3
Dog d2 = (Dog)a2; //4
}
}
把猫转换成狗,是不对的。后面注释为4的代码是无法正常赋值的。
Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Dog
at ClassCastExceptionTest.main(ClassCastExceptionTest.java:13)
2、 不同classloader加载相同类型类之间的转换
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
public class ClassCastExceptionTest {
/**
* @param args
*/
public static void main(String[] args) throws Exception {
File file = new File(Thread.currentThread().getContextClassLoader().getResource("").getPath());
URL[] urls = {file.toURL()};
URLClassLoader classloader1 = new URLClassLoader(urls, ClassLoader.getSystemClassLoader().getParent());
Class classloader1Animal1 = classloader1.loadClass("Dog");
Dog dog1 = (Dog)classloader1Animal1.newInstance();
URLClassLoader classloader2 = new URLClassLoader(urls, ClassLoader.getSystemClassLoader().getParent());
Class classloader1Animal2 = classloader1.loadClass("Dog");
Dog dog2 = (Dog)classloader1Animal1.newInstance();
dog1 = dog2;
}
}
代码中我们看到dog1=dog2,这样赋值是会抛异常的。
Exception in thread "main" java.lang.ClassCastException: Dog cannot be cast to Dog
at ClassCastExceptionTest.main(ClassCastExceptionTest.java:17)
以后大家遇到classCastException的时候要注意了,不一定是强制类型转换导致的,也有可能不同的classloader加载了相同的类,然后这个类不同的实例进行赋值。
(一)问题
项目中需要对文件做md5sum,分两步走:1、对文件流的每个字节用md5实例进行update,然后进行digest。2、digest返回长度为16的byte数组,一般我们需要把byte数组转成16进制字符串(很多开源的md5加密算法如此实现,真正的原因还不是很理解,可能是便于查看和传输)。具体的实现代码如下:
/**
* 对文件进行md5 sum操作
* @param checkFile 要进行做md5 sum的文件
* @return
*/
public static String md5sum(File checkFile){
String md5sumResult = "";
if(checkFile == null || (!checkFile.exists())){
return md5sumResult;
}
MessageDigest digest = MessageDigest.getInstance("MD5");
InputStream is = new FileInputStream(checkFile);
byte[] buffer = new byte[8192];
int read = 0;
try {
while( (read = is.read(buffer)) > 0) {
digest.update(buffer, 0, read);
}
byte[] md5sum = digest.digest();
BigInteger bigInt = new BigInteger(1, md5sum);
md5sumResult = bigInt.toString(16);
}
catch(IOException e) {
throw new RuntimeException("Unable to process file for MD5", e);
}
finally {
try {
is.close();
}
catch(IOException e) {
throw new RuntimeException("Unable to close input stream for MD5 calculation", e);
}
}
return md5sumResult;
}
其中黄色背景色的转换方式是有问题的。为什么用bigint转16进制会有问题呢,原因是bigint进行16进制转换的时候第一个0被自动去掉了.
(二)正确解决方式
那正确的方式是怎么样的呢?下面有两种不同的转换方式,但是原理其实是一致的。
第一种正确的方式(由王建提供):
/**
* 将字节数组转换为16进制字符串
*
* @param buffer
* @return
*/
public static String toHex(byte[] buffer) {
StringBuffer sb = new StringBuffer(buffer.length * 2);
for (int i = 0; i < buffer.length; i++) {
sb.append(Character.forDigit((buffer[i] & 240) >> 4, 16));
sb.append(Character.forDigit(buffer[i] & 15, 16));
}
return sb.toString();
}
第二种正确的方式:
public static String bytes2HexString(byte[] b) {
String ret = "";
for (int i = 0; i < b.length; i++) {
String hex = Integer.toHexString(b[i] & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
ret += hex;
}
return ret;
}
(三)问题分析
Md5算法对任何长度的字符串进行编码最后输出是128位长整数,也就是长度为16的byte数组。我们项目调用的是jdk实现的md5算法,所以一般是没问题的。
接下来我们要处理的事情,分别循环数组,把每个字节转换成2个16进制字符,也就是说每4位转成一个16进制字符。
上面正确的两种方式也就是做了这样的事情。
第一种方式:
Character.forDigit((buffer[i] & 240) >> 4, 16)把字节的高4位取出右移4位换算成int,然后通过forDigit转换成16进制字符
Character.forDigit(buffer[i] & 15, 16)把字节的低4位取出换算成int,然后通过forDigit转换成16进制字符
第二种方式:
Integer.toHexString(b[i] & 0xFF)把整个字节转成int,然后toHexString也就是做高4位和低4位的运算。但是这个方法如果高四位是0的话就不输出任何东西,
所以在输出的字符前加0即可。
b[i] & 0xFF就是把byte转成int,为什么用与oxff做与运算,是因为如果b[i]是负数的话,从8位变成32位会补1,所以需要与0xff做与运算,可以把前面的24位全部清零,又可以表示成原来的字节了。
附:
尽量使用开源提供的工具包,比如:
org.apache.commons.codec.digest.DigestUtils.md5Hex(InputStream data)来对文件流进行md5即可,更加方便,可靠。
早期的java程序员可能只要懂基本语法,还有少数的项目经验就可以找到一份比较好的工作。Java和java社区的发展,更多的人了解它,深入它。现在java程序员了解一些语法我看还远远不够了,对于jvm的了解和深入是非常重要的。网民的增多,网站的刚性需求,很多网站面临高性能,高并发等等一系列的问题。没有深入jvm的java程序员是很难写出高质量高并发的代码(也许一棒子打死所有人了,但我想绝大部分是肯定的)。
Osgi也许你并不陌生,但是他底层的实现机制你可能没去了解过。如果你是个打破砂锅问到底的人,你肯定会想知道osgi是如何做到的。但是你没有了解jvm的类加载体系,你肯定很难理解osgi是如果做到类隔离等一系列的问题。不过想完整理解osgi还需要其他很多方面的知识,但是它基本的机制还是的了解jvm的类加载机制。
Java类库有些包只是定义了一个标准,具体的实现都是由具体的供应商来提供。Java与数据库连接就是一个很好的例子。Java.sql类库只是定义了java与数据库连接的标准,那么与mysql就需要msyql的驱动,oracle就需要oracle的驱动,而java.sql类库是由bootstrap classloader加载,驱动包中的类是由system classloader来加载,不同类加载器加载的类是无法相互认识,所以自然也无法正常提供功能了。jvm又是提供了什么机制让他们交互呢?如果你确实对这些问题毫无头绪的话,那么我觉得你真的要好好理解下jvm类加载体系。
这篇文章主要是介绍下jvm类加载的机制基础知识。关于其他相关涉及,有时间的话,我会单独写文章来介绍。
1、 java类加载器
1.1 Bootstrap classloader:sun jdk是用c++实现,所以在代码中你是无法获取此加载器的实例。此加载器主要负责加载$JAVA_HOME/jre/lib/rt.jar。java类中获取结果为null,这里可以用一个例子跑下证明:
public class Test {
public static void main(String[] arg) throws Exception{
ClassLoader classloader = Test.class.getClassLoader();
System.out.println(classloader);
System.out.println(classloader.getParent());
System.out.println(classloader.getParent().getParent());
}
}
输出结果:
sun.misc.Launcher$AppClassLoader@19821f
sun.misc.Launcher$ExtClassLoader@addbf1
null
最后输出的null就是代表bootstrap classloader。
1.2 Extension classloader:主要加载扩展功能的jar,$JAVA_HOME/jre/lib/ext/*.jar。
1.3 System classloader:加载claspath中的jar包。
1.4自定义 classloader:主要加载你指定的目录中的包和类。
2、 双亲委派模型
系统运行时,我们请求加载String类,此时System Classloader自己不找classpath中的包,把请求转发给Extension Classloader,但它也不做查找,又转发给Bootstrap Classloader,但是它发现自己没有parent了。于是他在rt.jar包中找到String类并加载到jvm中提供使用。Jvm为什么要这么实现呢?其实和java安全体系有关。假设jvm不是这么实现,我们自定义一个String类,做一些破坏,那么运行jvm的机器肯定要受到损坏。具体例子:
public class String {
public static void main(String[] args) {
System.out.println("hello world");
}
}
我们运行自定义String类的时候报错了,说没有main方法,可我们定义的明明有的,嘿嘿,委派机制的缘故最后加载到的是由bootstrap classloader在rt.jar包中的String,那个类是没有main方法,因此报错了。
3、 类隔离
jvm中的类是:类加载器+包名+类名。比如:URLClassLoader1,URLClassLoader2分别加载com.test.Test的时候会加载两次,因为每个classloader中的类对于其他classloader来说是隔离的,不认识的。例子:
import java.net.URL;
import java.net.URLClassLoader;
public class CustomClassloaderTest {
public static void main(String[] args) throws Exception {
URL url = new URL("file:/g:/");
URLClassLoader ucl = new URLClassLoader(new URL[]{url});
Class c = ucl.loadClass("Yang");
c.newInstance();
System.out.println(c.getClassLoader());
URLClassLoader ucl2 = new URLClassLoader(new URL[]{url});
Class c2 = ucl2.loadClass("Yang");
c2.newInstance();
System.out.println(c2.getClassLoader());
}
}
大家把Yang类存在g盘下。
public class Yang {
static {
System.out.println("Yang");
}
}
运行结果:
Yang
java.net.URLClassLoader@c17164
Yang
java.net.URLClassLoader@61de33
看到每次加载Yang类的时候都输出Yang,说明Yang类被加载了两次。
如果你不确信,可以修改下代码,让同一classloader加载Yang类两次
import java.net.URL;
import java.net.URLClassLoader;
public class CustomClassloaderTest {
public static void main(String[] args) throws Exception {
URL url = new URL("file:/g:/");
URLClassLoader ucl = new URLClassLoader(new URL[]{url});
Class c = ucl.loadClass("Yang");
c.newInstance();
System.out.println(c.getClassLoader());
Class c2 = ucl.loadClass("Yang");
c2.newInstance();
System.out.println(c2.getClassLoader());
}
}
看看输出结果:
Yang
java.net.URLClassLoader@c17164
java.net.URLClassLoader@c17164
结果中只输出了一次Yang。因此可以证明我们最开始说的类隔离。
4、 线程上下文类加载器
我们理解了双亲委派模型,那么目前只有由下向上单向寻找类(system->extension->bootstrap)
。我们在最开始的时候说过,java.sql包中的类由bootstrap或extension classloader加载,而mysql驱动包是在classpath中由system来加载,但bootstrap中的类是无法找到system classloader中的类,此时靠线程上下文类加载器来解决。线程上下文类加载器主要就是能让jvm类加载模型具有了向下寻找的可能,bootstrap->extension->system,如果不做任何设置,线程上下文类加载器默认是system classloader。本来这里想写一个例子的,可是有点麻烦,所以下次单独写一篇关于这方面的知识。
以前我在小公司,完成项目功能是终极目标。开发人员很害怕需求变化,因为他们改怕了。那问题出在哪里呢?后来我仔细想想,是没有做测试造成。那开发人员为什么如此害怕需求变化,我举个例子,a服务给b服务和c服务调用,后来需求改变,导致a服务无法满足b服务,能完成自身的功能是天大的事,于是没有和别人沟通把a服务直接改了。项目上线,突然有一天客户打电话说你们网站这里出问题,那里出问题,以前都不会的啊。你们怎么弄的。于是根据页面错误信息,开发人员很快找到错误根源,原来a服务改动,导致b服务不正常。而d,e,f服务依赖于b,那么导致d,e,f相关功能都出错了。立马动手改,改完上线,能知道的问题都没了,哈哈,真高兴,可是不能高兴太早哇,也许还有潜在bug。
软件的bug是无法避免,但是我们可以尽量减少bug,不断提升代码质量。刚我也说过,上述问题造成的原因是没有做测试。测试包括很多了,单元测试、集成测试和功能测试等等。既然测试如此重要,每完成一个类都能进行测试。
以前也许你比较纠结,没有好的工具,现在java社区非常活跃,我们可以选择的太多太多了:junit4,jmock,mockito,easymock,TestNg等等。如果你用过grails,那么你更清楚,此类快速开发框架已经帮我们集成好了。使用起来非常简单。所以今天我主要讲述下grails的单元测试。
假设需求:我们给每个用户分配工作,每个人都要完成两件事情,第一件事情:根据自己的用户名返回欢迎信息;第二件事情:根据自己的地址返回国家地区。
详细设计
用户信息类:
package com.test.domian
class User {
int id
String name
String address
static constraints = {
}
}
工作服务接口:
package com.test.services
class WorkService {
/**
* 根据用户名返回欢迎字符
* @param userName
* @return
*/
def processWorkOne(String userName) {
}
/**
* 根据地址返回地区
* @param address
* @return
*/
def processWorkTwo(String address){
}
}
用户工作服务:
package com.test.services
import com.test.domian.User
class UserService {
def workService
def doWork() {
def userList = User.list()
userList.each {
it.name = workService.processWorkOne(it.name)
it.address = workService.processWorkTwo(it.address)
}
}
}
我们重点来看下测试类:
package com.test.services
import grails.test.*
import com.test.domian.User
class UserServiceTests extends GrailsUnitTestCase {
protected void setUp() {
super.setUp()
}
protected void tearDown() {
super.tearDown()
}
void testDoWork() {
//构造数据,类似于数据库存在三条记录
def user1 = new User(id:1, name:"lucy", address:"hangzhou")
def user2 = new User(id:2, name:"lily", address:"wenzhou")
def user3 = new User(id:3, name:"lilei", address:"beijing")
mockDomain User, [user1, user2, user3]
//mock WorkService接口的processWorkOne方法和processWorkTwo方法
def workControl = mockFor(WorkService)
def userCount = User.count()
while(userCount-- > 0){
workControl.demand.processWorkOne(1..1){String userName ->
return "hello world, " << userName
}
workControl.demand.processWorkTwo(1..1){String address ->
return "location in " << address
}
}
def workService = workControl.createMock()
//把构造好的workservice传给userservice
UserService userService = new UserService()
userService.workService = workService
userService.doWork()
def user4 = User.findById(1)
assertEquals "hello world, lucy", user4.name
assertEquals "location in hangzhou", user4.address
}
}
以下着重来具体说明:
1、
mockDomain方法就是构造数据,包括domain类的动态方法都可以使用,比如:save(),list(),findby*()等。代码中的User.count(); User.list();就是因为调用了mockDomain方法才可以正常使用。如果是集成测试的话,grails会帮我们构造好,可以直接使用。但这里是单元测试,所以需要自己mock。
2、mockFor方法就是给WorkService构造一个对象,然后给workControl对象的demand代理创建两个UserService中用的processWorkOne和processWorkTwo方法,代码中用到了1..1,表示mock对象只能调用这个方法一次,为什么要循环三次设置processWorkOne和processWorkTwo方法呢?因为我们在UserService是对三个对象分别进行调用处理这两件事情。也许你会想,干嘛不直接把1..3(最少调用一次,最多调用三次)。是的,我最开始也是这么来处理,可是单元测试就是同不过。
如果把UserService类中的
workControl.demand.processWorkOne(1..1){String userName ->
return "hello world, " << userName
}
改成
workControl.demand.processWorkOne(1..3){String userName ->
return "hello world, " << userName
}
然后把
UserServiceTests类中的:
userList.each {
it.name = workService.processWorkOne(it.name)
it.address = workService.processWorkTwo(it.address)
}
改成
userList.each {
it.name = workService.processWorkOne(it.name)
it.name = workService.processWorkOne(it.name)
it.name = workService.processWorkOne(it.name)
it.address = workService.processWorkTwo(it.address)
}
单元测试可以通过,但是改成这样
userList.each {
it.name = workService.processWorkOne(it.name)
it.name = workService.processWorkOne(it.name)
it.address = workService.processWorkTwo(it.address)
it.name = workService.processWorkOne(it.name)
}
单元测试通不过。
以上就是表明1..3的含义:这个方法要连续被调用至少一次,至多三次。
但是有的人说我在UserService中就要这么写
userList.each {
it.name = workService.processWorkOne(it.name)
it.name = workService.processWorkOne(it.name)
it.address = workService.processWorkTwo(it.address)
it.name = workService.processWorkOne(it.name)
}
那我要怎么改单元测试才能通过?
我们把UserServiceTests的demand这段代码
workControl.demand.processWorkOne(1..1){String userName ->
return "hello world, " << userName
}
workControl.demand.processWorkTwo(1..1){String address ->
return "location in " << address
}
改成
workControl.demand.processWorkOne(1..2){String userName ->
return "hello world, " << userName
}
workControl.demand.processWorkTwo(1..1){String address ->
return "location in " << address
}
workControl.demand.processWorkOne(1..1){String address ->
return "location in " << address
}
这样就通过了。
以上就是说明构造出来的函数只能按照构造的顺序调用。今天就是因为这个花了我好长时间啊,希望我理解是正确的。如有不对,请留言纠正。
Grails工程与maven集成
Grails其实也有自己的一些项目管理命令,如:grails package,grails test-app,grails war等。但是公司现在基本上都是用maven来管理项目,所以从管理上进行统一的目的,我们也让grails工程由maven来管理。
Grails与maven集成是靠maven插件机制。
接下来描述下集成的步骤:
1、在$home/.m2/settings.xml中配置plugin group
<settings>
…
<pluginGroups>
<pluginGroup>org.grails</pluginGroup>
</pluginGroups>
</settings>
没有配置之前,要运行grails:help命令要这样写:mvn org.grails:help,有了配置之后我们就可以这么写:mvn grails:help。
2、我们创建一个maven管理的grails工程
Mvn org.apache.maven.plugins:maven-archetype-plugin:2.0-alpha-4:generate
-DarchetypeGroupId=org.grails
-DarchetypeArtifactId=grails-maven-archetype
-DarchetypeVersion=1.2.0
-DgroupId=example -DartifactId=my-app
grails-maven-archetype这里用的是1.2.0版本,好像maven3集成的版本比这个要新。
3、进入my-app当前目录,运行mvn initialize
在运行过程中可能会出现如下问题:
Resolving plugin JAR dependencies …
:: problems summary ::
:::: WARNINGS
module not found: org.hibernate#hibernate-core;3.3.1.GA
那么你在application.properties文件中添加plugins.hibernate=1.3.2
plugins.tomcat=1.3.2两个插件。最后运行mvn compile重新编译工程。
4、我们是用springsource tool suite开发,导入工程。项目中需要对excel操作,我们采用jxl.jar开源包。在pom文件中配置如下内容:
<dependency>
<groupId>jxl</groupId>
<artifactId>jxl</artifactId>
<version>2.4.2</version>
</dependency>
重新编译下工程,但是引用jxl包中类的文件还是报错,说找不到类。这怎么回事呢?我也很纳闷,一般maven工程都是这样就可以。
后来在官方文档上看到一句话:pom=true。只要把这句话加到conf/BuildConfig.groovy文件中的grails.project.dependency.resolution方法中。如图:
重新编译,但是还是报错。Ide还是无法引用jxl包中的类。后来发现在grails tools中找到了一个命令。
执行过后,已经加入到了grails dependencies中。
为什么会这样呢,是因为有两套机制造成的。
第一套机制:maven自身管理项目的机制。
第二套机制:grails也有自己的一套管理机制。
l 在绝大多数情况下maven通过grails的集成插件可以对工程进行打包,部署,运行测试等管理。
l Grails可以通过自己的grails war等命令进行打包,部署,运行测试等管理。
l Grails也可以通过配置pom=true让grails那套管理机制用pom中的配置,不使用自己的管理机制。此时你就不用在conf/BuildConfig.groovy中管理依赖和资源库等配置。
大学期间,我热爱观看央视“赢在中国”节目。学到了很多做事的方法和做人的方式,虽然对于什么是创业,在创业中会遇到什么问题等等一些都未知,但那份年轻人的冲动和对于创业的兴奋已经被激发到极点。毕业找工作,我毅然选择了一家从事旅游行业的创业型互联网小公司。
面试阶段,一个相互选择的阶段。在大学期间,我运用J2EE技术给某朋友成功建立一家从事游戏虚拟物品交易平台,无形中已经培养了一定的需求沟通,需求挖掘,设计,项目管理等能力。加上我是一张“白纸”(价值观等都未受到任何公司的影响)和对于创业的那份冲劲。老板理所当然的选择了我,由我来负责一个项目的全部工作。公司给我提供我认为过得去的工资,也是我所喜好的创业型小公司,我认为很有前途(年少无知,不知后路的艰辛)。我也选择了公司。这就是我第一家的公司。
工作阶段,充当孙悟空的阶段。最简单的开发模式,最简单的项目管理方式,最简单的上线过程,最简单的线上故障处理。一切都是那么简单。简单的让我换第二份工作的时候让人觉得这几年的工作都毫无进步可言。
简单的开发模式,老板一个概念的产生,没有产品,直接抛到开发,开发要么模仿别人的网站要么自己捉摸该怎么实现,然后进入编码,没有代码review,没有单元测试,没有回归测试,不关注代码风格,不注重代码质量。功能实现了就算完了。后来改进,有产品分析设计产生prd,开发人员按照prd进行开发,有时进行部分重构,代码质量也没有太多的提升。
简单的项目管理方式,概念出来,开发人员大致揣测出老板的意图之后,开始分模块,估算时间,分配人员功能模块。老板看到项目时间需要3个月,老板对我们说,不可能需要这么长时间吧,给你们一个月半的时间给我完成吧,最后经过讨价还价,老板说再加半个月,你们不能再说了,最后我们被老板“强奸”了。项目也有版本控制,但是没有分支,只有主干,多个不同时间点的需求上线都在同一个主干上开发,导致有时候因为后面时间点的需求影响了前面时间点需求的上线。
最简单的上线过程,开发人员自学linux系统管理,自己通过ftp把主干代码上传部署,而且都是老板访问不了网站我们才知道出故障了,接着开发人员在主干上改bug,改完bug重新部署,发现之前的bug没了,出现了新的4个bug。继续修改bug,部署…无限循环。
最简单的线上故障处理,数据库负载过高、web服务器负载过高、服务器硬件坏了、网络线路,机房断电等等问题出现之后,唯一的办法就是停止网站服务进行修复。
每周五下午例会,讨论的问题都没有积累下来,没有被分享给其他团队成员,更不可能分享给新员工。
上面的流程基本上都是由开发人员负责,开发人员此时就是多角色,类似孙悟空,需要不断转换角色。更重要的是,上述所有流程和项目管理都只是关注项目自身。没有关注团队知识的积累,人员的培训。本质上就是不关注“成长”。我作为项目经理,我承认自己之前的不足,但值得庆幸的是我在离开之前做了一些比较有意义的事情,我让部门开发人员每个星期轮流做分享,让整个部门的人能够学到更多的知识。
后来来到淘宝,在这里能学到很多东西,因为这里有完善的培训体系,注重员工的个人成长。一个小插曲,我在新员工手册里看到sprint这个单词,我想在业内算比较牛的公司竟然还会把spring写成sprint,后来接触到了scrum敏捷开发,我才知道原来sprint是scrum的一个迭代周期。那个羞啊。哈哈。
来到淘宝,猛然有一种柳暗花明又一村的感觉,以前我做的事情都有涉及到,但在脑海中没有一个成形的框架,零零散散。
下面来讲讲跟我之前不一样的地方。
开发模式:
l 周五的双周pk,产品排好需求优先级,项目经理根据团队的人力资源pk需求,哪些可以完成,哪些人力不够。这里要着重强调工时,每个人每天都是按照4小时来算,其他4小时主要学习,更好的完成工作。4个小时是根据团队平时工作效率来计算的,也有可能是5或6个小时。不同时期不同项目每个人都有不同的变化曲线。
l Pk下来的需求,技术团队在周一进行任务拆分,然后大家领取各自的任务。
l 每个团队有自己的任务墙(故事墙),主要就是让大家在每天的15分钟晨会上列出各自的每天要完成的任务。
l 每天早上15分钟的晨会,一个是让大家都能在某个时间点之前赶到公司开会,另一个就是让大家清楚自己今天要做什么。晨会主要描述:1 我昨天做了什么 2 我遇到了哪些问题,自己解决了可以简单分享给同事,解决了不了可以让团队来帮忙一起解决。 3 今天我要做什么
l 每双周要做回顾,看看出现了什么问题,哪些地方可以再改进。
l 最后就是分享,有技术分享,业务分享。
这个开发模式对比以前有以下几个优点:
l 团队资源的合理利用,不会出现老板说几个月完成然后底下的人拼命的加班,让大家对技术的兴趣越来越高,工作的越来越快乐。成长也越来越快。
l 让大家明确知道自己今天要做什么。
l 分享,不但自己是分享的参与者也会是分享的发起人。不管是哪种角色,你都能学到很多很多。通过分享,团队的进步会非常快。
开发流程:
l 编码、单元测试
l Findbugs
l Mvn test
l Code review(重构,然后从头开始)
l 提交代码到svn
这个开发流程主要关注的是代码的提升,保证代码的质量,通过代码审查让尽早发现不合理的地方。
上线流程:
l 提前一个星期申请上线
l 单元测试
l 提测给测试团队
l 打包
l 发上线计划,预发冒烟,发布生产环境
这个流程我不是很熟悉,所以不作评论了。
有了这些比较优秀的模式和流程,也需要工具的配合。
代码版本控制:svn,并发开发的需求需要用到分支,主干代码尽量保证随时可以上线。
项目的管理:maven,开发模式,测试模式,生产模式配置的切换,也可以和hudson进行持续集成。
让我最有感触的就是淘宝非常注重知识的积累和员工的成长。在这里我感觉我真的成长了,有踏实的感觉,少了浮躁。这篇文章并不是说小公司不好,也是因为在之前那家公司接触的面广,所以来淘宝知道自己哪些对于自己更重要,但也并不鼓励你毕业就去小公司,因为对你的成长不好。每个人的路都是唯一的,大家喜欢怎么走就看大家自己的了,每个选择都是独一无二的,这样才能活出独一无二的生活,绚烂的生活。
开发过程中遇到了一些数据传输安全性问题,一个很重要实际需求,客户端加密的数据在服务端要解密回来还要进行一些处理。
脑中立马跳出几种解决方法:
1、直接使用MD5进行加密好了,可是MD5是不可逆的算法,而某些数据到达服务器端需要解密出来进行一些处理。看来不满足实际需求。
2、那可以尝试下DES,3DES或者AES等一些对称算法加密处理,想想挺好的,对称算法的效率也挺快。可是密钥该怎么从服务端安全的传递到客户端呢,这个问题不解决,加密还是如同虚设。
3、最后一种方案那就是使用RSA非对称算法,这个算法的好处就是服务器端自己维护私钥,把公钥开放给客户端,有人在网络上监听到公钥和加密后的数
据也没关系,因为加密的数据需要私钥才能解的开。也许大家都熟悉https协议,其实这种协议就是用RSA非对称算法来实现,但是大家肯定也有感受,用
https的时候网页打开的速度会比http要慢很多。我也考虑到这点,于是做了一个基准测试,在服务端写了个测试类,结果让我大吃一惊,2g的cpu循
环100次用私钥去解密竟然花了我50000多毫秒,那我循环10000次呢,靠,竟然花了几分钟。那如果采用这种方案去实现的话,应用程序的性能会被这
些解密动作所拉下。没办法哦,又只能放弃此类方案。
4、山重水复疑无路,柳暗花明又一村。突然脑中又蹦出另外一种想法,还是使用对称算法,关于密钥的传递可以采用DiffieHellman协议。
于是乎,上网查了一些资料,发现java类似的算法还是可以查的到,但是单有java也不行,我要在客户端加密,java端进行解密,所以还需要有
JavaScript的类似算法。最后在enano-1.1.7这个php开源的电子商务网站内找到了相关信息。无意间发现这个开源程序的一篇wiki,
详细介绍了一套安全解决方案(http://enanocms.org/News:Article/2008/02/20/Diffie_Hellman_key_exchange_implemented)。
以上只是我的思路,但还没有写个应用程序测试过。那么光有理论没有实践也不行,那就建个工程实现一下呗。嘿嘿。
case:用户注册。
case描述:客户在客户端填写一些信息,提交之前通过密钥把用户名和密码进行加密,服务端需要把用户名和密码解密回来进行进行处理,一个很重要的
处理就是,给密码加盐值,然后进行MD5加密,也许你会问,为什么这个动作不能在客户端做呢,其实也是可以的,但是为了不想让黑客知道我密码加密的体制所
以放到服务端进行。
case UML:
设计到的一些类和文件:
其中用到了base64,主要就是解决了中文乱码问题。
实现过程中的一些总结:
1、对于安全算法等一些总结,我用到一些相关算法类都是可以单独拿来用的。而jdk中也有支持的相关类,可以看看jca和jce。这两个扩展包其实并没有真正的实现,他们只是对这些安全问题的抽象。真正的实现有sunjce和Bouncy Castle。
2、对于算法本身定义的理解很重要,AES支持128,192,256位的密钥,但每次被加密的一定要是128位的内容,一般我们被加密的都是超过此长度的,那可以这么来处理:
把要加密的内容进行分组处理。解密也是类似。
最后的总结:此次实践只是模拟了一个场景,还可以运用很多场景中,OpenSSL,OpenID都可以运用。不是我说的,我也是看了找了相关的材
料,OpenSSL
java的项目还在建设中,php是有的,大家可以去找下,openid也是有用到的,这里推荐一个开源的项目openid4java,也可以去查查看,
有时间去看看里面的源码还是不错的。如果你也啥地方还不明白或者需要里面用到的一些代码,可以和我沟通交
流,msn:yangpingyu@gmail.com.
fastdfs-apache-module主要作用就是配合sotrage存储器以http方式下载文件,更重要的是解决了storage同步带来的延迟。也许熟悉fastdfs的朋友们知道,以前通过tracker来跳转也可以解决或其他方式也可以解决,舍取就要看大家的应用了。但是fishman(fastdfs作者)做过测试,性能绝对是fastdfs-apache-module要高。
生产环境中我肯定要用一些性能比较高的软件喽。因此把下载方式改成用fast-apache-module。以下是我的使用步骤和遇到的一些问题。前提条件:1、已经安装好fastdfs,而且版本是
FastDFS_v2.02.tar.gz或以上。2、已经安装好apache2.0或以上。
第一步,下载
fastdfs-apache-module。
第二步,编译fastdfs-apache-module。分以下几种情况。
1 、如果apache是你自己安装的,并且apache文件目录在/usr/local/apache2。那么你可以直接运行make,然后make install。
2、 如果apache是你自己安装的,apache目录在/opt/apache2,那么首先你得把Makefile文件的以下几处地方修改。
第七行改成:top_srcdir=/opt/apache2
第八行改成:top_builddir=/opt/apache2
第九行改成:include /opt/apache2/build/special.mk
第十二行改成:APXS=/opt/apache2/bin/apxs
第十三行改成:APACHECTL=/opt/apache2/bin/apachectl
改完后,然后执行make和make install命令。
3、 如果是系统自带的httpd,那么你就比较麻烦了。
3.1 首先安装httpd-devel包。建议使用centos的yum进行安装,souhu的源比较好用,速度挺快的。
3.2 类似的也要改Makefile文件了。
第七行改成:top_srcdir=/etc/httpd
第八行改成:top_builddir=/etc/httpd
第九行改成:include /etc/httpd/build/special.mk
第十二行改成:APXS=/usr/sbin/apxs
第十三行改成:APACHECTL=/usr/sbin/apachectl
保存。
3.3 ln -s /etc/httpd/build /usr/lib64/httpd/build(为了让第九行找到special.mk)
3.4 make
3.5 make install
如果正常编译的话,在/etc/fdfs目录下多了一个mod_fastdfs.conf文件,在${apache安装根目录}/modules目录下生成
mod_fastdfs.so文件。
第三步:修改httpd的配置文件httpd.conf。
1、LoadModule fastdfs_module modules/mod_fastdfs.so
2、
<Location /M00>
sethandler fastdfs
</Location>
3、设置DocumentRoot为:${fastdfs_base_path}/data
第四步:ln -s ${fastdfs_base_path}/data ${fastdfs_base_path}/data/M00
第五步:修改/etc/fdfs/mod_fastdfs.conf配置文件,文件中对每个设置字段都有注释
第七步:重启apache。
以上就是fastdfs-apache-module安装的具体过程。仅供参考。
在介绍本文之前,我向大家介绍下一个非常棒的分布式文件系统fastdfs,关于她的具体介绍和优点我不做详细介绍了,有关资料可以访问:
http://linux.chinaunix.net/bbs/forum-75-1.html 。
web2.0海量小文件的存储是所有系统架构师必须要面对的一个问题。
这几天一直在忙着给公司部署分布式文件系统。也看了几个大型公司的分布式文件系统的架构:flicker,taobao,拍拍网。深入理解各自应用的场景,弄明白了很多个为什么之后,自己将要为公司部署的分布式文件系统架构也慢慢浮出水面了。
架构随着业务发展而逐步改变的,比如类似淘宝这样的访问量,可能需要cdn来加速静态资源(js,css)和用户上传的业务相关的图片而一般访问量不是特别大的网站可以不用部署cdn,原因肯定很多,硬件成本和维护成本等等。
那么接下来我就分别介绍部署cdn的分布式文件系统架构和普通分布式文件系统架构。
(一)部署cdn的分布式文件系统架构:
涉及到的技术:lvs,haproxy(或nagix),squid,nagix(或apache),fastdfs。
有图有真相,先画个图。
接下来我对每层设置的意义进行解释下。
lvs7层代理:主要通过ip来找到自己合适的下层服务器。
haproxy或nginx4层代理:主要通过url hash找到合适的一台squid。最主要功能就是为了提高squid缓存的命中率。
squid集群:缓存所有用户访问的对象。
文件服务器集群:1、tracker服务器,主要管理文件存储的源storage等一些信息。 2、storage服务器就是实际文件的存储位置。最近fishman(fastdfs的作者)开发了一个apache module解决了延迟问题,那么我们可以不用启用tracker内嵌的服务器来跳转了,直接把http服务器配置在storage服务器上。给我们带来了很多方便。
(二)普通分布式文件系统架构:
(图片引自:
http://linux.chinaunix.net/bbs/thread-1062461-1-1.html)
这个架构没有涉及到cdn的部署,相比起来应该更容易理解了。对于tracker和storage的作用类似于在部署cdn的分布式文件系统架构部分介绍的一样。其中client我觉得有必要要解释下,这里的client有两层含义:1、相对于tracker和storage服务器来说,client是访问这些服务器的客户端。 2、应用程序的服务端。实际开发中,我们可以部署一台图片上传服务器来作为应用程序的服务端,也可以把图片上传服务直接写到应用程序中。
两种架构都是基于fastdfs分布式文件系统。所以你们了解fastdfs软件本身之后再看这篇文章也许会更有益。
两个项目中都使用了spring security安全框架,很多资料都是介绍spring security具体使用。今天我如果还是写这些东西就显得多余了,那么我从不同的角度来总结自己对这个框架的一些认识。
首先看看两个疑惑,然后我会逐步解释这两个疑惑。
第一个疑惑,spring security框架是spring的子框架,我就非常好奇spring security和spring是如何融合起来,确切的说,spring security定义的对象如何纳入spring ioc 容器中管理。研究到最后其实都是spring自身的一些知识,比如:自定义扩展xml schema,spring ioc启动。
第二个疑惑,spring security如何拦截用户的请求。这部分可以解读spring security源码可以得到答案。
彻底搞明白第一个疑惑之后,也许你以后自己写一个框架,就可以很方便的整合到spring中去了。对于框架开发工程师来说,开发新的框架之后能整合spring是必须的事情了,毕竟spring给我们所带来的好处是可想而知的。这也是我要彻底了解清楚原理的动力所在。废话一堆,进入主题吧~~~~
第一个疑惑最后涉及到两个方面的知识,spring ioc启动和spring可扩展xml schema。spring ioc有两个非常重要的概念,beanfactory和applicationContext,后者提供了更多更强的功能。为了避免过多的细节直接解读beanfactory的读取过程,xmlbeanfactory读取xml文件会经历如下两个过程:1、通过resource接口读取xml文件,转换成document。 2、从document中解析出bean的配置。具体详细过程请参照文章:
spring读取xml配置源代码分析(这篇文章一定要先看懂,不然后面很难继续)。看过我介绍大家看的那篇文章之后,其实也有所了解spring扩展xml schema机制了。如果还不是很清楚再结合这篇文章:
基于Spring可扩展Schema提供自定义配置支持。感觉有点东拼西凑的,呵呵,主要怕以后自己忘记了,所以才写篇blog。
第二个疑惑我们就看源代码吧。
<filter-mapping>
<filter-name>jcaptchaFilter</filter-name>
<url-pattern>/j_spring_security_check</url-pattern>
</filter-mapping>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>com.busyCity.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>FORWARD</dispatcher>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
web.xml中配置了
DelegatingFilterProxy,DelegatingFilterProxy调用FilterChainProxy的doFilter
public void doFilter(ServletRequest request, ServletResponse response)throws IOException, ServletException
{
if(currentPosition == additionalFilters.size())
{
if(FilterChainProxy.logger.isDebugEnabled())
FilterChainProxy.logger.debug((new StringBuilder()).append(fi.getRequestUrl()).append(" reached end of additional filter chain; proceeding with original chain").toString());
fi.getChain().doFilter(request, response);
} else
{
currentPosition++;
Filter nextFilter = (Filter)additionalFilters.get(currentPosition - 1);
if(FilterChainProxy.logger.isDebugEnabled())
FilterChainProxy.logger.debug((new StringBuilder()).append(fi.getRequestUrl()).append(" at position ").append(currentPosition).append(" of ").append(additionalFilters.size()).append(" in additional filter chain; firing Filter: '").append(nextFilter).append("'").toString());
nextFilter.doFilter(request, response, this);
}
}
这个方法就是循环调用我们用http命名空间配置的那些过滤器。然后根据不同的过滤器处理不同的内容。
我描述的都很简单,主要原因是做个记录,以后忘记了可以根据这个思路重新找到答案。不需要重新开始研究。呵呵。
一直以来,看了很多东西自己知道就完了,而我觉得作为互联网的一份子,应该懂得分享。
最近看了很多关于程立的一些演讲,学习到了soa实践中非常宝贵的经验。
最开始支付宝架构是单应用系统,采用分层架构模式(展现层+业务层+持久层),用过分层架构模式的人都很清楚业务层是最关键的一层,也是最容易造成臃肿和庞大的一层,所以需要合理的分离。而最佳实践应该是把业务层分为:facade层和业务逻辑层,也符合现在比较热的领域模型驱动,业务逻辑层主要负责业务本身的实现,facade层区分产品功能和决策。对于中小型而且项目业务需求也不怎么变的应用来说,是比较合理的架构。无论从学习成本和人力成本来说,都相对较低。但是业务不断扩张的应用来说,到最后开发成本和维护将会逐步提高。支付宝也是这个时候就开始着手对“对象”进行“组件”化,也就是接下来的第二个过程了。
从对象到组件化,我们首先要来看看组件这个概念。组件是功能职责相近类的集合。组件本身需要以下几点功能:1、属性 2、运行级别 3、引用 4、扩展 等。而那时比较符合支付宝这一思想的规范有osgi。具体关于osgi的相关内容可以自行google。如果用osgi会出现如下三个问题:
1、osgi平台如何在tomcat,jboss下运行。(现在tomcat,jboss等应用服务器都已经支持了,以前需要自己来扩展tomcat)
2、osgi使用起来比较麻烦和复杂,如何跟spring整合起来使用也是需要解决的问题,不过spring就是spring,05左右好像就对这方面开始研究了,现在作为一个子项目(Spring Dynamic Modules)。
3、osgi规范没有扩展这种功能,所以需要自己来对osgi的一些框架进行扩展,可以参照eclipse的一个osgi框架,或者也可以直接用此框架(Equinox)。
这些问题都解决了,那么“对象”进行“组件”过程就完成了。但是这个时候需要对企业内部的系统能够配合起来,那么可以对业务层进行服务化。也就是第三个过程了。
把组件都以服务方式部署出来,同时也需要管理好这些服务,那么此时可以引入esb(企业服务总线)中间件了。商业的和开源的都有,支付宝用的是开源的mule,不过对于某些方面,支付宝进行改造过,为了确保消息能够百分百不丢失,毕竟跟钱打交道嘛需要严谨。
其实这些基础平台搭建好后,还需要解决一个特别难的难题。功能服务化后,数据库也不再是集中式了,本地事务也不能保证了。这个时候需要引入分布式事务,数据库软件本身也提供分布式事务,但是对于需要高访问量和高性能的网站来说,可能需要换一种方式了。cap和base理论告诉我们可以适当放弃强一致性来达到其他两项性能。ws-transaction是分布式中间件,但是完成一个事务需要很多消息来沟通,至少大大增大了事务的中间过程会被中断掉。支付宝公司研究出了一些分布式事务模式,比如幂等性模式,补偿性模式,tcc模式等等。每个模式都有不同的应用场景,大家可以根据自己的业务特点来进行选取。
以上即是我最近学习的一些总结。让更多的人能学习好的技术和经验。让我们一起来分享吧。