月度归档:2015年10月

SpringMVC Controller介绍

来源:http://haohaoxuexi.iteye.com/blog/1753271

一、简介

在SpringMVC 中,控制器Controller 负责处理由DispatcherServlet 分发的请求,它把用户请求的数据经过业务处理层处理之后封装成一个Model ,然后再把该Model 返回给对应的View 进行展示。在SpringMVC 中提供了一个非常简便的定义Controller 的方法,你无需继承特定的类或实现特定的接口,只需使用@Controller 标记一个类是Controller ,然后使用@RequestMapping 和@RequestParam 等一些注解用以定义URL 请求和Controller 方法之间的映射,这样的Controller 就能被外界访问到。此外Controller 不会直接依赖于HttpServletRequest 和HttpServletResponse 等HttpServlet 对象,它们可以通过Controller 的方法参数灵活的获取到。为了先对Controller 有一个初步的印象,以下先定义一个简单的Controller :

@Controller
public class MyController {
@RequestMapping ( "/showView" )
public ModelAndView showView() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName( "viewName" );
modelAndView.addObject( " 需要放到 model 中的属性名称 " , " 对应的属性值,它是一个对象 " );
return modelAndView;
}
}

在上面的示例中,@Controller 是标记在类MyController 上面的,所以类MyController 就是一个SpringMVC Controller 对象了,然后使用@RequestMapping(“/showView”) 标记在Controller 方法上,表示当请求/showView.do 的时候访问的是MyController 的showView 方法,该方法返回了一个包括Model 和View 的ModelAndView 对象。这些在后续都将会详细介绍。

二、使用 @Controller 定义一个 Controller 控制器

@Controller 用于标记在一个类上,使用它标记的类就是一个SpringMVC Controller 对象。分发处理器将会扫描使用了该注解的类的方法,并检测该方法是否使用了@RequestMapping 注解。@Controller 只是定义了一个控制器类,而使用@RequestMapping 注解的方法才是真正处理请求的处理器,这个接下来就会讲到。

单单使用@Controller 标记在一个类上还不能真正意义上的说它就是SpringMVC 的一个控制器类,因为这个时候Spring 还不认识它。那么要如何做Spring 才能认识它呢?这个时候就需要我们把这个控制器类交给Spring 来管理。拿MyController 来举一个例子

@Controller
public class MyController {
@RequestMapping ( "/showView" )
public ModelAndView showView() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName( "viewName" );
modelAndView.addObject( " 需要放到 model 中的属性名称 " , " 对应的属性值,它是一个对象 " );
return modelAndView;
}
}

这个时候有两种方式可以把MyController 交给Spring 管理,好让它能够识别我们标记的@Controller 。

第一种方式是在SpringMVC 的配置文件中定义MyController 的bean 对象。

<bean class="com.host.app.web.controller.MyController"/>

第二种方式是在SpringMVC 的配置文件中告诉Spring 该到哪里去找标记为@Controller 的Controller 控制器。

< context:component-scan base-package = "com.host.app.web.controller" >
< context:exclude-filter type = "annotation"
expression = "org.springframework.stereotype.Service" />
</ context:component-scan >

注:

上面 context:exclude-filter 标注的是不扫描 @Service 标注的类

三、使用 @RequestMapping 来映射 Request 请求与处理器

可以使用@RequestMapping 来映射URL 到控制器类,或者是到Controller 控制器的处理方法上。当@RequestMapping 标记在Controller 类上的时候,里面使用@RequestMapping 标记的方法的请求地址都是相对于类上的@RequestMapping 而言的;当Controller 类上没有标记@RequestMapping 注解时,方法上的@RequestMapping 都是绝对路径。这种绝对路径和相对路径所组合成的最终路径都是相对于根路径“/ ”而言的。

@Controller
public class MyController {
@RequestMapping ( "/showView" )
public ModelAndView showView() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName( "viewName" );
modelAndView.addObject( " 需要放到 model 中的属性名称 " , " 对应的属性值,它是一个对象 " );
return modelAndView;
}

}

在这个控制器中,因为MyController 没有被@RequestMapping 标记,所以当需要访问到里面使用了@RequestMapping 标记的showView 方法时,就是使用的绝对路径/showView.do 请求就可以了。

@Controller
@RequestMapping ( "/test" )
public class MyController {
@RequestMapping ( "/showView" )
public ModelAndView showView() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName( "viewName" );
modelAndView.addObject( " 需要放到 model 中的属性名称 " , " 对应的属性值,它是一个对象 " );
return modelAndView;
}

}

这种情况是在控制器上加了@RequestMapping 注解,所以当需要访问到里面使用了@RequestMapping 标记的方法showView() 的时候就需要使用showView 方法上@RequestMapping 相对于控制器MyController 上@RequestMapping 的地址,即/test/showView.do 。

(一)使用 URI 模板

URI 模板就是在URI 中给定一个变量,然后在映射的时候动态的给该变量赋值。如URI 模板http://localhost/app/{variable1}/index.html ,这个模板里面包含一个变量variable1 ,那么当我们请求http://localhost/app/hello/index.html 的时候,该URL 就跟模板相匹配,只是把模板中的variable1 用hello 来取代。在SpringMVC 中,这种取代模板中定义的变量的值也可以给处理器方法使用,这样我们就可以非常方便的实现URL 的RestFul 风格。这个变量在SpringMVC 中是使用@PathVariable 来标记的。

在SpringMVC 中,我们可以使用@PathVariable 来标记一个Controller 的处理方法参数,表示该参数的值将使用URI 模板中对应的变量的值来赋值。

@Controller
@RequestMapping ( "/test/{variable1}" )
public class MyController {

@RequestMapping ( "/showView/{variable2}" )
public ModelAndView showView( @PathVariable String variable1, @PathVariable ( "variable2" ) int variable2) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName( "viewName" );
modelAndView.addObject( " 需要放到 model 中的属性名称 " , " 对应的属性值,它是一个对象 " );
return modelAndView;
}
}

在上面的代码中我们定义了两个URI 变量,一个是控制器类上的variable1 ,一个是showView 方法上的variable2 ,然后在showView 方法的参数里面使用@PathVariable 标记使用了这两个变量。所以当我们使用/test/hello/showView/2.do 来请求的时候就可以访问到MyController 的showView 方法,这个时候variable1 就被赋予值hello ,variable2 就被赋予值2 ,然后我们在showView 方法参数里面标注了参数variable1 和variable2 是来自访问路径的path 变量,这样方法参数variable1 和variable2 就被分别赋予hello 和2 。方法参数variable1 是定义为String 类型,variable2 是定义为int 类型,像这种简单类型在进行赋值的时候Spring 是会帮我们自动转换的,关于复杂类型该如何来转换在后续内容中将会讲到。

在上面的代码中我们可以看到在标记variable1 为path 变量的时候我们使用的是@PathVariable ,而在标记variable2 的时候使用的是@PathVariable(“variable2”) 。这两者有什么区别呢?第一种情况就默认去URI 模板中找跟参数名相同的变量,但是这种情况只有在使用debug 模式进行编译的时候才可以,而第二种情况是明确规定使用的就是URI 模板中的variable2 变量。当不是使用debug 模式进行编译,或者是所需要使用的变量名跟参数名不相同的时候,就要使用第二种方式明确指出使用的是URI 模板中的哪个变量。

除了在请求路径中使用URI 模板,定义变量之外,@RequestMapping 中还支持通配符“* ”。如下面的代码我就可以使用/myTest/whatever/wildcard.do 访问到Controller 的testWildcard 方法。
@Controller
@RequestMapping ( "/myTest" )
public class MyController {
@RequestMapping ( "*/wildcard" )
public String testWildcard() {
System. out .println( "wildcard------------" );
return "wildcard" ;
}
}

 

(二)使用 @RequestParam 绑定 HttpServletRequest 请求参数到控制器方法参数

@RequestMapping ( "requestParam" )
ublic String testRequestParam( @RequestParam(required=false) String name, @RequestParam ( "age" ) int age) {
return "requestParam" ;
}

在上面代码中利用@RequestParam 从HttpServletRequest 中绑定了参数name 到控制器方法参数name ,绑定了参数age 到控制器方法参数age 。值得注意的是和@PathVariable 一样,当你没有明确指定从request 中取哪个参数时,Spring 在代码是debug 编译的情况下会默认取更方法参数同名的参数,如果不是debug 编译的就会报错。此外,当需要从request 中绑定的参数和方法的参数名不相同的时候,也需要在@RequestParam 中明确指出是要绑定哪个参数。在上面的代码中如果我访问/requestParam.do?name=hello&age=1 则Spring 将会把request 请求参数name 的值hello 赋给对应的处理方法参数name ,把参数age 的值1 赋给对应的处理方法参数age 。

在@RequestParam 中除了指定绑定哪个参数的属性value 之外,还有一个属性required ,它表示所指定的参数是否必须在request 属性中存在,默认是true ,表示必须存在,当不存在时就会报错。在上面代码中我们指定了参数name 的required 的属性为false ,而没有指定age 的required 属性,这时候如果我们访问/requestParam.do 而没有传递参数的时候,系统就会抛出异常,因为age 参数是必须存在的,而我们没有指定。而如果我们访问/requestParam.do?age=1 的时候就可以正常访问,因为我们传递了必须的参数age ,而参数name 是非必须的,不传递也可以。

(三)使用 @CookieValue 绑定 cookie 的值到 Controller 方法参数

@RequestMapping ( "cookieValue" )
public String testCookieValue( @CookieValue ( "hello" ) String cookieValue, @CookieValue String hello) {
System. out .println(cookieValue + "-----------" + hello);
return "cookieValue" ;
}

 

在上面的代码中我们使用@CookieValue 绑定了cookie 的值到方法参数上。上面一共绑定了两个参数,一个是明确指定要绑定的是名称为hello 的cookie 的值,一个是没有指定。使用没有指定的形式的规则和@PathVariable 、@RequestParam 的规则是一样的,即在debug 编译模式下将自动获取跟方法参数名同名的cookie 值。

(四)使用 @RequestHeader 注解绑定 HttpServletRequest 头信息到 Controller 方法参数

 

  1. @RequestMapping ( "testRequestHeader" )
  2. public String testRequestHeader( @RequestHeader ( "Host" ) String hostAddr, @RequestHeader String Host, @RequestHeader String host ) {
  3.     System. out .println(hostAddr + "-----" + Host + "-----" + host );
  4.     return "requestHeader" ;
  5. }

 

在上面的代码中我们使用了 @RequestHeader 绑定了 HttpServletRequest 请求头 host 到 Controller 的方法参数。上面方法的三个参数都将会赋予同一个值,由此我们可以知道在绑定请求头参数到方法参数的时候规则和 @PathVariable 、 @RequestParam 以及 @CookieValue 是一样的,即没有指定绑定哪个参数到方法参数的时候,在 debug 编译模式下将使用方法参数名作为需要绑定的参数。但是有一点 @RequestHeader 跟另外三种绑定方式是不一样的,那就是在使用 @RequestHeader 的时候是大小写不敏感的,即 @RequestHeader(“Host”) 和 @RequestHeader(“host”) 绑定的都是 Host 头信息。记住在 @PathVariable 、 @RequestParam 和 @CookieValue 中都是大小写敏感的。

(五) @RequestMapping 的一些高级应用

在RequestMapping 中除了指定请求路径value 属性外,还有其他的属性可以指定,如params 、method 和headers 。这样属性都可以用于缩小请求的映射范围。

1.params属性

params 属性用于指定请求参数的,先看以下代码。

  1. @RequestMapping (value= "testParams" , params={ "param1=value1" , "param2" , "!param3" })
  2. public String testParams() {
  3.    System. out .println( "test Params..........." );
  4.    return "testParams" ;
  5. }

 

在上面的代码中我们用@RequestMapping 的params 属性指定了三个参数,这些参数都是针对请求参数而言的,它们分别表示参数param1 的值必须等于value1 ,参数param2 必须存在,值无所谓,参数param3 必须不存在,只有当请求/testParams.do 并且满足指定的三个参数条件的时候才能访问到该方法。所以当请求/testParams.do?param1=value1&param2=value2 的时候能够正确访问到该testParams 方法,当请求/testParams.do?param1=value1&param2=value2&param3=value3 的时候就不能够正常的访问到该方法,因为在@RequestMapping 的params 参数里面指定了参数param3 是不能存在的。

2.method属性

method 属性主要是用于限制能够访问的方法类型的。

  1. @RequestMapping (value= "testMethod" , method={RequestMethod. GET , RequestMethod. DELETE })
  2. public String testMethod() {
  3.    return "method" ;
  4. }

在上面的代码中就使用method 参数限制了以GET 或DELETE 方法请求/testMethod.do 的时候才能访问到该Controller 的testMethod 方法。

3.headers属性

使用headers 属性可以通过请求头信息来缩小@RequestMapping 的映射范围。

  1. @RequestMapping (value= "testHeaders" , headers={ "host=localhost" , "Accept" })
  2. public String testHeaders() {
  3.    return "headers" ;
  4. }

headers 属性的用法和功能与params 属性相似。在上面的代码中当请求/testHeaders.do 的时候只有当请求头包含Accept 信息,且请求的host 为localhost 的时候才能正确的访问到testHeaders 方法。

(六)以 @RequestMapping 标记的处理器方法支持的方法参数和返回类型

1. 支持的方法参数类型

(1 )HttpServlet 对象,主要包括HttpServletRequest 、HttpServletResponse 和HttpSession 对象。 这些参数Spring 在调用处理器方法的时候会自动给它们赋值,所以当在处理器方法中需要使用到这些对象的时候,可以直接在方法上给定一个方法参数的申明,然后在方法体里面直接用就可以了。但是有一点需要注意的是在使用HttpSession 对象的时候,如果此时HttpSession 对象还没有建立起来的话就会有问题。

(2 )Spring 自己的WebRequest 对象。 使用该对象可以访问到存放在HttpServletRequest 和HttpSession 中的属性值。

(3 )InputStream 、OutputStream 、Reader 和Writer 。 InputStream 和Reader 是针对HttpServletRequest 而言的,可以从里面取数据;OutputStream 和Writer 是针对HttpServletResponse 而言的,可以往里面写数据。

(4 )使用@PathVariable 、@RequestParam 、@CookieValue 和@RequestHeader 标记的参数。

   (5 )使用@ModelAttribute 标记的参数。

   (6 )java.util.Map 、Spring 封装的Model 和ModelMap 。 这些都可以用来封装模型数据,用来给视图做展示。

(7 )实体类。 可以用来接收上传的参数。

(8 )Spring 封装的MultipartFile 。 用来接收上传文件的。

(9 )Spring 封装的Errors 和BindingResult 对象。 这两个对象参数必须紧接在需要验证的实体对象参数之后,它里面包含了实体对象的验证结果。

2. 支持的返回类型

(1 )一个包含模型和视图的ModelAndView 对象。

(2 )一个模型对象,这主要包括Spring 封装好的Model 和ModelMap ,以及java.util.Map ,当没有视图返回的时候视图名称将由RequestToViewNameTranslator 来决定。

(3 )一个View 对象。这个时候如果在渲染视图的过程中模型的话就可以给处理器方法定义一个模型参数,然后在方法体里面往模型中添加值。

(4 )一个String 字符串。这往往代表的是一个视图名称。这个时候如果需要在渲染视图的过程中需要模型的话就可以给处理器方法一个模型参数,然后在方法体里面往模型中添加值就可以了。

(5 )返回值是void 。这种情况一般是我们直接把返回结果写到HttpServletResponse 中了,如果没有写的话,那么Spring 将会利用RequestToViewNameTranslator 来返回一个对应的视图名称。如果视图中需要模型的话,处理方法与返回字符串的情况相同。

(6 )如果处理器方法被注解@ResponseBody 标记的话,那么处理器方法的任何返回类型都会通过HttpMessageConverters 转换之后写到HttpServletResponse 中,而不会像上面的那些情况一样当做视图或者模型来处理。

(7 )除以上几种情况之外的其他任何返回类型都会被当做模型中的一个属性来处理,而返回的视图还是由RequestToViewNameTranslator 来决定,添加到模型中的属性名称可以在该方法上用@ModelAttribute(“attributeName”) 来定义,否则将使用返回类型的类名称的首字母小写形式来表示。使用@ModelAttribute 标记的方法会在@RequestMapping 标记的方法执行之前执行。

(七)使用 @ModelAttribute 和 @SessionAttributes 传递和保存数据

SpringMVC 支持使用 @ModelAttribute 和 @SessionAttributes 在不同的模型和控制器之间共享数据。 @ModelAttribute 主要有两种使用方式,一种是标注在方法上,一种是标注在 Controller 方法参数上。

当 @ModelAttribute 标记在方法上的时候,该方法将在处理器方法执行之前执行,然后把返回的对象存放在 session 或模型属性中,属性名称可以使用 @ModelAttribute(“attributeName”) 在标记方法的时候指定,若未指定,则使用返回类型的类名称(首字母小写)作为属性名称。关于 @ModelAttribute 标记在方法上时对应的属性是存放在 session 中还是存放在模型中,我们来做一个实验,看下面一段代码。

@Controller
@RequestMapping ( "/myTest" )
public class MyController {

@ModelAttribute ( "hello" )
public String getModel() {
System. out .println( "-------------Hello---------" );
return "world" ;
}

@ModelAttribute ( "intValue" )
public int getInteger() {
System. out .println( "-------------intValue---------------" );
return 10;
}

@RequestMapping ( "sayHello" )
public void sayHello( @ModelAttribute ( "hello" ) String hello, @ModelAttribute ( "intValue" ) int num, @ModelAttribute ( "user2" ) User user, Writer writer, HttpSession session) throws IOException {
writer.write( "Hello " + hello + " , Hello " + user.getUsername() + num);
writer.write( "\r" );
Enumeration enume = session.getAttributeNames();
while (enume.hasMoreElements())
writer.write(enume.nextElement() + "\r" );
}

@ModelAttribute ( "user2" )
public User getUser() {
System. out .println( "---------getUser-------------" );
return new User(3, "user2" );
}
}

当我们请求 /myTest/sayHello.do 的时候使用 @ModelAttribute 标记的方法会先执行,然后把它们返回的对象存放到模型中。最终访问到 sayHello 方法的时候,使用 @ModelAttribute 标记的方法参数都能被正确的注入值。执行结果如下图所示:

由执行结果我们可以看出来,此时 session 中没有包含任何属性,也就是说上面的那些对象都是存放在模型属性中,而不是存放在 session 属性中。那要如何才能存放在 session 属性中呢?这个时候我们先引入一个新的概念 @SessionAttributes ,它的用法会在讲完 @ModelAttribute 之后介绍,这里我们就先拿来用一下。我们在 MyController 类上加上 @SessionAttributes 属性标记哪些是需要存放到 session 中的。看下面的代码:

@Controller
@RequestMapping ( "/myTest" )
@SessionAttributes (value={ "intValue" , "stringValue" }, types={User. class })
public class MyController {

@ModelAttribute ( "hello" )
public String getModel() {
System. out .println( "-------------Hello---------" );
return "world" ;
}

@ModelAttribute ( "intValue" )
public int getInteger() {
System. out .println( "-------------intValue---------------" );
return 10;
}

@RequestMapping ( "sayHello" )
public void sayHello(Map<String, Object> map, @ModelAttribute ( "hello" ) String hello, @ModelAttribute ( "intValue" ) int num, @ModelAttribute ( "user2" ) User user, Writer writer, HttpServletRequest request) throws IOException {
map.put( "stringValue" , "String" );
writer.write( "Hello " + hello + " , Hello " + user.getUsername() + num);
writer.write( "\r" );
HttpSession session = request.getSession();
Enumeration enume = session.getAttributeNames();
while (enume.hasMoreElements())
writer.write(enume.nextElement() + "\r" );
System. out .println(session);
}

@ModelAttribute ( "user2" )
public User getUser() {
System. out .println( "---------getUser-------------" );
return new User(3, "user2" );
}
}

 

在上面代码中我们指定了属性为 intValue 或 stringValue 或者类型为 User 的都会放到 Session 中,利用上面的代码当我们访问 /myTest/sayHello.do 的时候,结果如下:

仍然没有打印出任何 session 属性,这是怎么回事呢?怎么定义了把模型中属性名为 intValue 的对象和类型为 User 的对象存到 session 中,而实际上没有加进去呢?难道我们错啦?我们当然没有错,只是在第一次访问 /myTest/sayHello.do 的时候 @SessionAttributes 定义了需要存放到 session 中的属性,而且这个模型中也有对应的属性,但是这个时候还没有加到 session 中,所以 session 中不会有任何属性,等处理器方法执行完成后 Spring 才会把模型中对应的属性添加到 session 中。所以当请求第二次的时候就会出现如下结果:

当 @ModelAttribute 标记在处理器方法参数上的时候,表示该参数的值将从模型或者 Session 中取对应名称的属性值,该名称可以通过 @ModelAttribute(“attributeName”) 来指定,若未指定,则使用参数类型的类名称(首字母小写)作为属性名称。

@Controller
@RequestMapping ( "/myTest" )
public class MyController {

@ModelAttribute ( "hello" )
public String getModel() {
return "world" ;
}

@RequestMapping ( "sayHello" )
public void sayHello( @ModelAttribute ( "hello" ) String hello, Writer writer) throws IOException {
writer.write( "Hello " + hello);
}
}

 

在上面代码中,当我们请求/myTest/sayHello.do 的时候,由于MyController 中的方法getModel 使用了注解@ModelAttribute 进行标记,所以在执行请求方法sayHello 之前会先执行getModel 方法,这个时候getModel 方法返回一个字符串world 并把它以属性名hello 保存在模型中,接下来访问请求方法sayHello 的时候,该方法的hello 参数使用@ModelAttribute(“hello”) 进行标记,这意味着将从session 或者模型中取属性名称为hello 的属性值赋给hello 参数,所以这里hello 参数将被赋予值world ,所以请求完成后将会在页面上看到Hello world 字符串。

@SessionAttributes 用于标记需要在Session 中使用到的数据,包括从Session 中取数据和存数据。@SessionAttributes 一般是标记在Controller 类上的,可以通过名称、类型或者名称加类型的形式来指定哪些属性是需要存放在session 中的。

@Controller
@RequestMapping ( "/myTest" )
@SessionAttributes (value={ "user1" , "blog1" }, types={User. class , Blog. class })
public class MyController {

@RequestMapping ( "setSessionAttribute" )
public void setSessionAttribute(Map<String, Object> map, Writer writer) throws IOException {
User user = new User(1, "user" );
User user1 = new User(2, "user1" );
Blog blog = new Blog(1, "blog" );
Blog blog1 = new Blog(2, "blog1" );
map.put( "user" , user);
map.put( "user1" , user1);
map.put( "blog" , blog);
map.put( "blog1" , blog1);
writer.write( "over." );
}

@RequestMapping ( "useSessionAttribute" )
public void useSessionAttribute(Writer writer, @ModelAttribute ( "user1" ) User user1, @ModelAttribute ( "blog1" ) Blog blog1) throws IOException {
writer.write(user1.getId() + "--------" + user1.getUsername());
writer.write( "\r" );
writer.write(blog1.getId() + "--------" + blog1.getTitle());
}

@RequestMapping ( "useSessionAttribute2" )
public void useSessionAttribute(Writer writer, @ModelAttribute ( "user1" ) User user1, @ModelAttribute ( "blog1" ) Blog blog1, @ModelAttribute User user, HttpSession session) throws IOException {
writer.write(user1.getId() + "--------" + user1.getUsername());
writer.write( "\r" );
writer.write(blog1.getId() + "--------" + blog1.getTitle());
writer.write( "\r" );
writer.write(user.getId() + "---------" + user.getUsername());
writer.write( "\r" );
Enumeration enume = session.getAttributeNames();
while (enume.hasMoreElements())
writer.write(enume.nextElement() + " \r" );
}

@RequestMapping ( "useSessionAttribute3" )
public void useSessionAttribute( @ModelAttribute ( "user2" ) User user) {

}
}

在上面代码中我们可以看到在MyController 上面使用了@SessionAttributes 标记了需要使用到的Session 属性。可以通过名称和类型指定需要存放到Session 中的属性,对应@SessionAttributes 注解的value 和types 属性。当使用的是types 属性的时候,那么使用的Session 属性名称将会是对应类型的名称(首字母小写)。当value 和types 两个属性都使用到了的时候,这时候取的是它们的并集,而不是交集,所以上面代码中指定要存放在Session 中的属性有名称为user1 或blog1 的对象,或类型为User 或Blog 的对象。在上面代码中我们首先访问/myTest/setSessionAttribute.do ,该请求将会请求到MyController 的setSessionAttribute 方法,在该方法中,我们往模型里面添加了user 、user1 、blog 和blog1 四个属性,因为它们或跟类上的@SessionAttributes 定义的需要存到session 中的属性名称相同或类型相同,所以在请求完成后这四个属性都将添加到session 属性中。接下来访问/myTest/useSessionAttribute.do ,该请求将会请求MyController 的useSessionAttribute(Writer writer, @ModelAttribute(“user1”) User user1, @ModelAttribute(“blog1”) Blog blog) 方法,该方法参数中用@ModelAttribute 指定了参数user1 和参数blog1 是需要从session 或模型中绑定的,恰好这个时候session 中已经有了这两个属性,所以这个时候在方法执行之前会先绑定这两个参数。执行结果如下图所示:

接下来访问/myTest/useSessionAttribute2.do ,这个时候请求的是上面代码中对应的第二个useSessionAttribute 方法,方法参数user 、user1 和blog1 用@ModelAttribute 声明了需要session 或模型属性注入,我们知道在请求/myTest/setSessionAttribute.do 的时候这些属性都已经添加到了session 中,所以该请求的结果会如下图所示:

接下来访问/myTest/useSessionAttribute3.do ,这个时候请求的是上面代码中对应的第三个useSessionAttribute 方法,我们可以看到该方法的方法参数user 使用了@ModelAttribute(“user2”) 进行标记,表示user 需要session 中的user2 属性来注入,但是这个时候我们知道session 中是不存在user2 属性的,所以这个时候就会报错了。执行结果如图所示:

(八)定制自己的类型转换器

在通过处理器方法参数接收 request 请求参数绑定数据的时候,对于一些简单的数据类型 Spring 会帮我们自动进行类型转换,而对于一些复杂的类型由于 Spring 没法识别,所以也就不能帮助我们进行自动转换了,这个时候如果我们需要 Spring 来帮我们自动转换的话就需要我们给 Spring 注册一个对特定类型的识别转换器。 Spring 允许我们提供两种类型的识别转换器,一种是注册在 Controller 中的,一种是注册在 SpringMVC 的配置文件中。聪明的读者看到这里应该可以想到它们的区别了,定义在 Controller 中的是局部的,只在当前 Controller 中有效,而放在 SpringMVC 配置文件中的是全局的,所有 Controller 都可以拿来使用。

1. 在 @InitBinder 标记的方法中定义局部的类型转换器

我们可以使用 @InitBinder 注解标注在 Controller 方法上,然后在方法体里面注册数据绑定的转换器,这主要是通过 WebDataBinder 进行的。我们可以给需要注册数据绑定的转换器的方法一个 WebDataBinder 参数,然后给该方法加上 @InitBinder 注解,这样当该 Controller 中在处理请求方法时如果发现有不能解析的对象的时候,就会看该类中是否有使用 @InitBinder 标记的方法,如果有就会执行该方法,然后看里面定义的类型转换器是否与当前需要的类型匹配。

@Controller
@RequestMapping ( "/myTest" )
public class MyController {

@InitBinder
public void dataBinder(WebDataBinder binder) {
DateFormat dateFormat = new SimpleDateFormat( "yyyyMMdd" );
PropertyEditor propertyEditor = new CustomDateEditor(dateFormat, true ); // 第二个参数表示是否允许为空
binder.registerCustomEditor(Date. class , propertyEditor);
}

@RequestMapping ( "dataBinder/{date}" )
public void testDate( @PathVariable Date date, Writer writer) throws IOException {
writer.write(String.valueOf (date.getTime()));
}

}

在上面的代码中当我们请求 /myTest/dataBinder/20121212.do 的时候, Spring 就会利用 @InitBinder 标记的方法里面定义的类型转换器把字符串 20121212 转换为一个 Date 对象。这样定义的类型转换器是局部的类型转换器,一旦出了这个 Controller 就不会再起作用。类型转换器是通过 WebDataBinder 对象的 registerCustomEditor 方法来注册的,要实现自己的类型转换器就要实现自己的 PropertyEditor 对象。 Spring 已经给我们提供了一些常用的属性编辑器,如 CustomDateEditor 、 CustomBooleanEditor 等。

PropertyEditor 是一个接口,要实现自己的 PropertyEditor 类我们可以实现这个接口,然后实现里面的方法。但是 PropertyEditor 里面定义的方法太多了,这样做比较麻烦。在 java 中有一个封装类是实现了 PropertyEditor 接口的,它是 PropertyEditorSupport 类。所以如果需要实现自己的 PropertyEditor 的时候只需要继承 PropertyEditorSupport 类,然后重写其中的一些方法。一般就是重写 setAsText 和 getAsText 方法就可以了, setAsText 方法是用于把字符串类型的值转换为对应的对象的,而 getAsText 方法是用于把对象当做字符串来返回的。在 setAsText 中我们一般先把字符串类型的对象转为特定的对象,然后利用 PropertyEditor 的 setValue 方法设定转换后的值。在 getAsText 方法中一般先使用 getValue 方法取代当前的对象,然后把它转换为字符串后再返回给 getAsText 方法。下面是一个示例:
@InitBinder
public void dataBinder(WebDataBinder binder) {
// 定义一个 User 属性编辑器
PropertyEditor userEditor = new PropertyEditorSupport() {

@Override
public String getAsText() {
// TODO Auto-generated method stub
User user = (User) getValue();
return user.getUsername();
}

@Override
public void setAsText(String userStr) throws IllegalArgumentException {
// TODO Auto-generated method stub
User user = new User(1, userStr);
setValue(user);
}
};
// 使用 WebDataBinder 注册 User 类型的属性编辑器
binder.registerCustomEditor(User. class , userEditor);
}

2. 实现 WebBindingInitializer 接口定义全局的类型转换器

如果需要定义全局的类型转换器就需要实现自己的 WebBindingInitializer 对象,然后把该对象注入到 AnnotationMethodHandlerAdapter 中,这样 Spring 在遇到自己不能解析的对象的时候就会到全局的 WebBindingInitializer 的 initBinder 方法中去找,每次遇到不认识的对象时, initBinder 方法都会被执行一遍。

 

Java代码

  1. public class MyWebBindingInitializer implements WebBindingInitializer {
  2.     @Override
  3.     public void initBinder(WebDataBinder binder, WebRequest request) {
  4.        // TODO Auto-generated method stub
  5.        DateFormat dateFormat = new SimpleDateFormat( "yyyyMMdd" );
  6.        PropertyEditor propertyEditor = new CustomDateEditor(dateFormat, true );
  7.        binder.registerCustomEditor(Date. class , propertyEditor);
  8.     }
  9. }

 

定义了这么一个 WebBindingInitializer 对象之后 Spring 还是不能识别其中指定的对象,这是因为我们只是定义了 WebBindingInitializer 对象,还没有把它交给 Spring , Spring 不知道该去哪里找解析器。要让 Spring 能够识别还需要我们在 SpringMVC 的配置文件中定义一个 AnnotationMethodHandlerAdapter 类型的 bean 对象,然后利用自己定义的 WebBindingInitializer 覆盖它的默认属性 webBindingInitializer 。

 

Xml代码

  1. < bean class = "org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" >
  2.    < property name = "webBindingInitializer" >
  3.        < bean class = "com.host.app.web.util.MyWebBindingInitializer" />
  4.    </ property >
  5. </ bean >

 

3.触发数据绑定方法的时间

当Controller处理器方法参数使用@RequestParam、 @PathVariable、@RequestHeader、@CookieValue和@ModelAttribute标记的时候都会触发 initBinder方法的执行,这包括使用WebBindingInitializer定义的全局方法和在Controller中使用 @InitBinder标记的局部方法。而且每个使用了这几个注解标记的参数都会触发一次initBinder方法的执行,这也意味着有几个参数使用了上 述注解就会触发几次initBinder方法的执行。

OpenLDAP使用及使用Java完成LDAP身份认证

来源:http://www.cnblogs.com/lanxuezaipiao/p/3664676.html

导读

LDAP(轻量级目录访问协议,Lightweight Directory Access Protocol)是实现提供被称为目录服务的信息服务。目录服务是一种特殊的数据库系统,其专门针对读取,浏览和搜索操作进行了特定的优化。目录一般用来包含描述性的,基于属性的信息并支持精细复杂的过滤能力。目录一般不支持通用数据库针对大量更新操作操作需要的复杂的事务管理或回卷策略。而目录服务的更新则一般都非常简单。这种目录可以存储包括个人信息、web链结、jpeg图像等各种信息。为了访问存储在目录中的信息,就需要使用运行在TCP/IP 之上的访问协议—LDAP。

LDAP目录中的信息是是按照树型结构组织,具体信息存储在条目(entry)的数据结构中。常见的例子是通讯簿,由以字母顺序排列的名字、地址和电话号码组成。

目录服务与关系数据库之间的主要区别在于:二者都允许对存储数据进行访问,只是目录主要用于读取,其查询的效率很高,而关系数据库则是为读写而设计的。也就是目录服务不适于进行频繁的更新,属于典型的分布式结构。

总结:对于查询操作多于更新操作的(认证)系统来说,使用OpenLDAP是一个比关系数据库如MySq、PostgreSQL等更好的选择。

LDAP的功能

在LDAP的功能模型中定义了一系列利用LDAP协议的操作,主要包含以下4部分:

查询操作:允许查询目录和取得数据,其查询性能比关系数据库好。

更新操作:目录的更新操作没关系数据库方便,更新性能较差,但也同样允许进行添加、删除、修改等操作。

复制操作:前面也提到过,LDAP是一种典型的分布式结构,提供复制操作,可将主服务器的数据的更新复制到设置的从服务器中。

认证和管理操作:允许客户端在目录中识别自己,并且能够控制一个会话的性质。

而本文所要将的OpenLDAP就是一个优秀的开源的LDAP实现。

OpenLDAP安装配置及疑惑解答

1. 安装和配置OpenLDAP

安装软件非常简单,但在配置过程中遇到了不少坎坷,不是服务启动不成功就是验证不成功。

具体的安装和配置方法网上一大把,但都参差不齐,主要是因为新旧版本的OpenLDAP不同,配置方法有很大的改动。

下面给出网上几个还算靠谱的Linux和Windows两个平台下安装该软件的方法:

1)ubuntu安装LDAP:安装方法靠谱,但配置说的不太清楚,配置注意事项看后面。

2)Ubuntu OpenLDAP Server:官方教程,最值得借鉴,是英文的,这里有中文版的,但没英文的清晰,说的比较简单。

3)Linux下安装openldap:二进制包安装方法,适用于非Ubuntu的Linux系统,稍微有点麻烦,在安装OpenlDAP之前还需要安装Berkeley DB,但配置灵活,可以自定义安装路径什么的。后面的配置也没说清楚,主要看安装方法。

4)Linux服务器部署系列之七—OpenLDAP篇:另一篇较详细的二进制安装方法及配置。

4)Windows下OpenLDAP的安装及使用:介绍了LDAP的一些基础知识和Windows下安装方法。

5)图文介绍openLDAP在windows上的安装配置:比较详细,值得一看。

上面给出的这几个链接虽然还不错,但还是欠缺了些什么?对,就是讲解,网上给出的教程都是手把手教你如何安装和配置,而没有说明版本差异、具体配置的含义及为什么这样配置,如果因为版本或环境差异,你按其方法配置不成功,你也不知道哪里出的问题,因此建议还是先熟悉LDAP的基础知识,配置文件含义然后再试着安装。

2. OpenLDAP疑惑解答

下面根据我自己的经验,给出几个安装和配置注意事项,供参考。

疑惑1:细心的人会发现有的教程说要配置主机DNS,添加与LDAP相关的域名,而大部分教程都没有提及这个,那么到底要不要配置呢?

解答:当然需要配置。安装好OpenLDAP后首先需要配置slapd.conf这个文件,其中里面有

suffix        "dc=example, dc=com"

这样一句需要自己配置,这两个dc代表什么意思呢?其实dc就是“domainComponent”,也就是域名的组成部分,准确的说是主机域名的 后缀组成部分,如果这里的配置与你的主机域名不对应的话,服务一般是启动不了的。那么怎么配置域名呢?Linux和Windows下的配置文件如下:

Linux下:/etc/hosts

Windows下:C:\Windows\System32\drivers\etc\hosts

需要在hosts文件里添加一条域名(如果没配置的话),格式如下:

127.0.1.1       hostname.example.com    hostname

比如我的主机名是min,并添加的域名配置是:

127.0.1.1       min.alexia.cn    min

那么相应的我就需要在slapd.conf里这样配置suffix:

suffix        "dc=alexia, dc=cn"

当然这里域名后缀不一定只有两级,也可以是hostname.example.com.cn,然后suffix就应该是“dc=example, dc=com, dc=cn”,这随便你怎么设置了,只要对应就行。

疑惑2:很多版本的slapd.conf里默认都配置了下面两个变量:

modulepath      /usr/lib/ldap
moduleload      back_@BACKEND@

这是什么意思?需要改动吗?

解答:这是数据库database的backend,一般slapd.conf里配置的database都是 bdb,也就是Berkeley DB,有的也许是hdb,其实也是Berkeley DB,只是两个不同的存储引擎(就像Mysql有MyISAM和InnoDB两个不同的存储引擎一样)。而modulepath和moduleload指 定了动态模块路径及动态装载的后端模块,因为OpenLDAP默认是用Berkeley DB存储数据的,如果你有动态的数据需要装载,那么就需要配置这两个参数,对于一般用户将这两个注释掉即可。

疑惑3:OpenLDAP默认采用Berkeley DB存储数据,那么可以换用其它的关系数据库吗?具体如何配置呢?

解答:当然可以。首先需要明确ldap数据模型来自RDBMS(关系数据库模型),而并没有指定一定是哪个 DB,只要是关系数据库都可以作为LDAP的后台,那么你为什么会想用其它的数据库代替自带的Berkeley DB呢?我想可能是性能相关了,对于少量数据你用哪个都可以,但若涉及到稍大点的数据,比如成千上万的用户查询,那么Berkeley DB的性能就不可观了,而且Berkeley DB管理起来也不太方便,毕竟对这个数据库熟悉的人不多,如果能换作我们经常使用的数据库,不仅性能得到提升,管理起来也十分容易,岂不是一举多得。

具体怎么配置了,请参考这篇文章:用postgresql作后台的openldap,以PostgreSQL作为例子进行讲解。

疑惑4:新旧版本的OpenLDAP到底有什么差异呢?

解答:简单一句话就是:旧版本的OpenLDAP配置文件一般是slapd.conf(路径可能是/etc/openldap,也可能是/usr/local/openldap,甚至可能是/usr/share/slapd/,不同版本不同安装不同系统都可能不同,可使用locate slapd.conf进行查找正确的路径),而新版本(我测试的新版本是2.4.31)的OpenLDAP服务运行时并不会读取该配置文件,而是从slapd.d目录(一般与slapd.conf在同一目录下)中读取相关信息,我们需要把该目录下的数据删掉,然后利用我们在slapd.conf里配置的信息重新生成配置数据。这也可能是你启动服务后运行ldap相关命令却出现“ldap_bind: Invalid credentials (49)”错误的主要原因。具体怎么重新生成配置数据请看参考资料。

疑惑5:自定义的ldif数据文件中的objectclass后的domain、top、organizationalUnit、inetOrgPerson等等都是什么意思,可以随便写吗?

解答:存储LDAP配置信息及目录内容的标准文本文件格式是LDIF(LDAP Interchange Format),使用文本文件来格式来存储这些信息是为了方便读取和修改,这也是其它大多数服务配置文件所采取的格式。LDIF文件常用来向目录导入或更 改记录信息,这些信息需要按照LDAP中schema的格式进行组织,并会接受schema 的检查,如果不符合其要求的格式将会出现报错信息。因此,ldif文件中的属性都定义在各大schema中,其中objectclass是对象的类属性, 不能随便填写,而应与schema中一致。一般slapd.conf文件的头部都包含了这些schema:

include         ../etc/openldap/schema/core.schema
include         ../etc/openldap/schema/cosine.schema
include         ../etc/openldap/schema/inetorgperson.schema
include         ../etc/openldap/schema/nis.schema
include         ../etc/openldap/schema/krb5-kdc.schema
include         ../etc/openldap/schema/RADIUS-LDAPv3.schema
include         ../etc/openldap/schema/samba.schema

其中前三个是比较重要的schema,定义了我们所需要的各个类,比如ldif中一般先定义一个根节点,其相应的objectclass一般是 domain和top,而根节点下的ou属性即定义组节点(group)的objectclass一般是 organizationalUnit,group下可以是group也可以是用户节点,用户节点的objectclass一般是 inetOrgPerson。而各个节点的一系列属性如用户节点的uid、mail、userPassword、sn等等都定义在schema中相关的 objectclass里,可以自己查找看看。

疑惑6:OpenLDAP认证用户uid时默认是不区分大小写的,也就是“alexia”与“AleXia”是同一个用户,在有些情况下这并不合理,能配置使得认证时能区分大小写吗?

解答:以我目前的经验来看,旧版本的OpenLDAP是可以配置区分大小写的,而新版本的OpenLDAP却配置不了。为什么这么说呢?

这里就涉及到“matching rules”这个概念了,即匹配规则,就是各个属性按什么样的规则进行匹配,比如是否区分大小写、是否进行数字匹配等等,这里有详细的官方匹配规则描述。比如旧版本的core.schema里有下面这样一段:

attributetype ( 0.9.2342.19200300.100.1.1
   NAME ( 'uid' 'userid' )
   DESC 'RFC1274: user identifier'
   EQUALITY caseIgnoreMatch
   SUBSTR caseIgnoreSubstringsMatch
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )

从字面上也可以看出,其中caseIgnoreMatchcaseIgnoreSubstringsMatch就定义了uid或userid属性匹配时不区分大小写,如果我们将其改为caseExactMatchcaseExactSubstringsMatch就表示用户uid认证时需要区分大小写,也就是“alexia”与“AleXia”同不同的用户,这很简单,在旧版本的OpenLDAP也行得通。

可是在新版本的OpenLDAP中却不行,新版本的core.schema文件中也包含这样一段:

#attributetype (  2.16.840.1.113730.3.1.217
#    NAME ( 'uid' 'userid' )
#    DESC 'RFC1274: user identifier'
#    EQUALITY caseIgnoreMatch
#    SUBSTR caseIgnoreSubstringsMatch
#    SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )

可惜是注释掉的,那我们取消注释然后改属性行不行呢?答案是不行,会报错:Duplicate attributeType: "2.16.840.1.113730.3.1.217”,也就是说该属性已经被定义了,然后我就去包含的所有schema中搜索uid属性的定义,结果却找不到定义,那么为什么还会报这个错误呢?后来一阵搜索,终于在这个帖子“slapd: built-in schema for uidNumber/gidNumber does not have ordering directive”知道了答案,原来新版本的OpenLDAP已经把uid属性定义schema硬编码到了slapd程序中,也就是无法在配置文件中修改了,真是坑!

针对这个问题,我给出两个不太好的解决方案:

  1. 下载OpenLDAP源码,找到定义uid属性匹配规则的地方,修改它然后重新编译。这个工作量不轻松,热爱研究源码的人可以尝试。
  2. 不 要用uid属性进行认证,我们可以自定义一个与用户一一对应的属性如user-id(不要与已有的属性重复就行),其配置与uid一模一样(即模仿 uid),然后用该属性作为认证的因子,建议重新建一个schema,然后配置好后include进slapd.conf中重启服务即可。具体怎么定义和 配置可以参考这篇文章。

我的主要经验也就这些。OpenLDAP也有客户端,如果你配置成功后,可以用客户端或写Java程序进行验证。

OpenLDAP客户端

OpenLDAP既有图形客户端也有网页客户端。

1. 图形客户端

主要有两个图形客户端:LdapBrowser282 (下载:LdapBrowser282.zip,下载解压后直接双击:lbe.bat 文件即可运行)和LdapAdmin(官方下载),使用都非常简单。

如下是两个客户端的界面,都需要先建立一个链接,填上相应的IP地址、端口和dn配置,然后连接即可获得你配置的数据。

LDAP Browser客户端:

LDAP Admin客户端:

 

2. 网页客户端

即 phpLDAPadmin,基于PHP的一个web应用,需要配置Apache服务器和PHP,具体的配置方法可参考“phpLDAPadmin 安装配置讲解,通过 Web 端来管理您的 LDAP 服务器”,我比较偷懒,直接使用的PHPnow全套服务,安装成功后大概是下面这样一个界面:

使用Java完成LDAP身份验证

下面借鉴网上资料提供一个简单的认证程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import java.util.Hashtable;
import javax.naming.AuthenticationException;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
 
public class LDAPAuthentication {
    private final String URL = "ldap://127.0.0.1:389/";
    private final String BASEDN = "ou=Tester,dc=alexia,dc=cn"// 根据自己情况进行修改
    private final String FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
    private LdapContext ctx = null;
    private final Control[] connCtls = null;
 
    private void LDAP_connect() {
        Hashtable<String, String> env = new Hashtable<String, String>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, FACTORY);
        env.put(Context.PROVIDER_URL, URL + BASEDN);
        env.put(Context.SECURITY_AUTHENTICATION, "simple");
         
        String root = "cn=manager,dc=alexia,dc=cn"// 根,根据自己情况修改
        env.put(Context.SECURITY_PRINCIPAL, root);   // 管理员
        env.put(Context.SECURITY_CREDENTIALS, "123456");  // 管理员密码
        
        try {
            ctx = new InitialLdapContext(env, connCtls);
            System.out.println( "认证成功" ); 
            
        } catch (javax.naming.AuthenticationException e) {
            System.out.println("认证失败:");
            e.printStackTrace();
        } catch (Exception e) {
            System.out.println("认证出错:");
            e.printStackTrace();
        }
        
        if (ctx != null) {
            try {
                ctx.close();
            }
            catch (NamingException e) {
                e.printStackTrace();
            }
        }
    }
 
    private String getUserDN(String uid) {
        String userDN = "";
        LDAP_connect();
        try {
            SearchControls constraints = new SearchControls();
            constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
            NamingEnumeration<SearchResult> en = ctx.search("", "uid=" + uid, constraints);
            if (en == null || !en.hasMoreElements()) {
                System.out.println("未找到该用户");
            }
            // maybe more than one element
            while (en != null && en.hasMoreElements()) {
                Object obj = en.nextElement();
                if (obj instanceof SearchResult) {
                    SearchResult si = (SearchResult) obj;
                    userDN += si.getName();
                    userDN += "," + BASEDN;
                } else {
                    System.out.println(obj);
                }
            }
        } catch (Exception e) {
            System.out.println("查找用户时产生异常。");
            e.printStackTrace();
        }
 
        return userDN;
    }
 
    public boolean authenricate(String UID, String password) {
        boolean valide = false;
        String userDN = getUserDN(UID);
 
        try {
            ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, userDN);
            ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
            ctx.reconnect(connCtls);
            System.out.println(userDN + " 验证通过");
            valide = true;
        } catch (AuthenticationException e) {
            System.out.println(userDN + " 验证失败");
            System.out.println(e.toString());
            valide = false;
        } catch (NamingException e) {
            System.out.println(userDN + " 验证失败");
            valide = false;
        }
 
        return valide;
    }
    
    public static void main(String[] args) {
        LDAPAuthentication ldap = new LDAPAuthentication();
        if(ldap.authenricate("gygtest", "jmwang") == true){
            System.out.println( "该用户认证成功" );
        }
    }
}

既可以作为普通程序的认证,也可以通过输出检查自己的配置是否正确。

LDAP扩展

LDAP的实现除了OpenLDAP外,还有其它,比如OpenDJ(Open source Directory services for the Java platform),它是一个新的LDAPv3相容目录服务,为Java平台开发,提供了一个高性能的,高度可用和安全的企业管理的身份商店。其简单的安 装过程中,结合了Java平台的力量,使OpenDJ简单和最快的目录服务器部署和管理。有兴趣的可以查阅相关资料。

参考资料

  • LDAP快速入门
  • 执行ldapadd 命令时报错:ldap_bind: Invalid credentials (49)
  • OpenLdap 简易教程
  • OpenLDAP学习笔记
  • OpenLDAP guide: Schema Specification
  • 使用java完成ldap身份验证

java command line structures

https://github.com/airlift/airline

Airline is a Java annotation-based framework for parsing Git like command line structures.

Latest release is 0.7, available from Maven Central.

<dependency>
<groupId>io.airlift</groupId>
<artifactId>airline</artifactId>
<version>0.7</version>
</dependency>

Here is a quick example:
public class Git
{
public static void main(String[] args)
{
CliBuilder<Runnable> builder = Cli.<Runnable>builder("git")
.withDescription("the stupid content tracker")
.withDefaultCommand(Help.class)
.withCommands(Help.class, Add.class);

builder.withGroup("remote")
.withDescription("Manage set of tracked repositories")
.withDefaultCommand(RemoteShow.class)
.withCommands(RemoteShow.class, RemoteAdd.class);

Cli<Runnable> gitParser = builder.build();

gitParser.parse(args).run();
}

public static class GitCommand implements Runnable
{
@Option(type = OptionType.GLOBAL, name = "-v", description = "Verbose mode")
public boolean verbose;

public void run()
{
System.out.println(getClass().getSimpleName());
}
}

@Command(name = "add", description = "Add file contents to the index")
public static class Add extends GitCommand
{
@Arguments(description = "Patterns of files to be added")
public List<String> patterns;

@Option(name = "-i", description = "Add modified contents interactively.")
public boolean interactive;
}

@Command(name = "show", description = "Gives some information about the remote <name>")
public static class RemoteShow extends GitCommand
{
@Option(name = "-n", description = "Do not query remote heads")
public boolean noQuery;

@Arguments(description = "Remote to show")
public String remote;
}

@Command(name = "add", description = "Adds a remote")
public static class RemoteAdd extends GitCommand
{
@Option(name = "-t", description = "Track only a specific branch")
public String branch;

@Arguments(description = "Remote repository to add")
public List<String> remote;
}
}

Assuming you have packaged this as an executable program named 'git', you would be able to execute the following commands:

$ git add -p file

$ git remote add origin git@github.com:airlift/airline.git

$ git -v remote show origin

Single Command Mode

Airline can also be used for simple, single-command programs:

@Command(name = "ping", description = "network test utility")
public class Ping
{
@Inject
public HelpOption helpOption;

@Option(name = {"-c", "--count"}, description = "Send count packets")
public int count = 1;

public static void main(String... args)
{
Ping ping = SingleCommand.singleCommand(Ping.class).parse(args);

if (ping.helpOption.showHelpIfRequested()) {
return;
}

ping.run();
}

public void run()
{
System.out.println("Ping count: " + count);
}
}

Assuming you have packaged this as an executable program named 'ping', you would be able to execute the following commands:

$ ping

$ ping -c 5

$ ping --help

Help System

Airline contains a fully automated help system, which generates man-page-like documentation driven by the Java annotations.

As you may have noticed in the git code above, we added Help.class to the cli. This command is provided by Airline and works as follows:

$ git help
usage: git [-v] <command> [<args>]

The most commonly used git commands are:
add       Add file contents to the index
help      Display help information
remote    Manage set of tracked repositories

See 'git help <command>' for more information on a specific command.

$ git help git
NAME
git - the stupid content tracker

SYNOPSIS
git [-v] <command> [<args>]

OPTIONS
-v
Verbose mode

COMMANDS
help
Display help information

add
Add file contents to the index

remote show
Gives some information about the remote <name>

remote add
Adds a remote

$ git help add
NAME
git add - Add file contents to the index

SYNOPSIS
git [-v] add [-i] [--] [<patterns>...]

OPTIONS
-i
Add modified contents interactively.

-v
Verbose mode

--
This option can be used to separate command-line options from the
list of argument, (useful when arguments might be mistaken for
command-line options

<patterns>
Patterns of files to be added

$ git help remote
NAME
git remote - Manage set of tracked repositories

SYNOPSIS
git [-v] remote
git [-v] remote add [-t <branch>]
git [-v] remote show [-n]

OPTIONS
-v
Verbose mode

COMMANDS
With no arguments, Gives some information about the remote <name>

show
Gives some information about the remote <name>

With -n option, Do not query remote heads

add
Adds a remote

With -t option, Track only a specific branch

$ git help remote show
NAME
git remote show - Gives some information about the remote <name>

SYNOPSIS
git [-v] remote show [-n] [--] [<remote>]

OPTIONS
-n
Do not query remote heads

-v
Verbose mode

--
This option can be used to separate command-line options from the
list of argument, (useful when arguments might be mistaken for
command-line options

<remote>
Remote to show

We have also, add Help.class as the default command for git, so if you execute git without any arguments, you will see the following:

$ git help
usage: git [-v] <command> [<args>]

The most commonly used git commands are:
add       Add file contents to the index
help      Display help information
remote    Manage set of tracked repositories

See 'git help <command>' for more information on a specific command.

For simple, single-command programs like ping, use the HelpOption option as shown in the example above. HelpOption handles the options -h and --help and provides the showHelpIfRequested() method to automatically show the following help output:

$ ping -h
NAME
ping - network test utility

SYNOPSIS
ping [(-c <count> | --count <count>)] [(-h | --help)]

OPTIONS
-c <count>, --count <count>
Send count packets

-h, --help
Display help information

使用memc-nginx和srcache-nginx模块构建高效透明的缓存机制

Nginx的Memc和SR Cache模块

缓存策略的改进

为了提高性能,几乎所有互联网应用都有缓存机制,其中Memcache是使用非常广泛的一个分布式缓存系统。众所周知,LAMP是非常经典的Web架构方式,但是随着Nginx的成熟,越来越多的系统开始转型为LNMP(Linux+Nginx+MySQL+PHP with fpm),这是因为Nginx采用基于事件机制的I/O多路复用思想设计,在高并发情况下其性能远远优于默认采用prefork模式的Apache,另外,相对于Apache,Nginx更轻量,同时拥有大量优秀的扩展模块,使得在Nginx上可以实现一些美妙的功能。

传统上,PHP中使用memcache的方法是使用php-memcache或php-memached扩展操作Memcache,然而在Nginx上有构建更高效缓存机制的方法,本文将首先介绍这种机制,然后介绍具体的操作步骤方法,最后将对这种机制和传统的PHP操作memcache的性能进行一个benchmark。

我们知道,Nginx的核心设计思想是事件驱动的非阻塞I/O。

Nginx被设计为可以配置I/O多路复用策略,在Unix系统中传统的多路复用是采用select或poll,但是这两个方法的问题是随着监听 socket的增加,性能会下降,因为在linux内核中是采用轮询的方式判断是否可以触发事件,换句话说算法的复杂度为O(N),而在较新的linux 内核中引入了复杂度为O(1)的epoll,因此Nginx在Linux下默认采用epoll,而在FreeBSD下默认采用kqueue作为I/O策 略。

即便是这样,传统的缓存策略仍可能造成效率低下,因为传统上是通过PHP操作memcache的,要执行PHP代码,Nginx就必然要和FastCGI 通信,同时也要进入PHP的生命周期,因此SAPI、PHP Core和Zend Engine的一系列逻辑会被执行。更糟糕的是,fpm和PHP可能会阻塞,因此破坏了Nginx的非阻塞性。(原文中此处表述有误,fastcgi与 nginx进行同步通信,但并不会破坏nginx i/o的非阻塞性,多谢agentzh给予指正)下图展示了在memcache命中时整个处理过程。

可以看到,即使Memcache命中,还是要进入PHP的生命周期。我们知道,目前很多互联网应用都使用RESTful规范进行设计,在RESTful应 用下,普遍使用uri和查询参数作为缓存的key,因此一种更高效的缓存策略是Nginx直接访问Memcache,并用$uri和$args等 Nginx内置变量设定缓存key规则,这样,当缓存命中时,Nginx可以跳过通过fastcgi和PHP通信的过程,直接从memcache中获取数 据并返回。memc-nginx和srcache-nginx正是利用这种策略提高了缓存的效率。下图是这种高效缓存策略的示意图(当memcache命 中时)。

模块介绍

memc-nginxsrcache-nginx模 块均为前淘宝工程师agentzh(章亦春)开发。其中memc模块扩展了Nginx标准的memcache模块,增加了set、add、delete等 memcache命令,而srcache则是为location增加了透明的基于subrequest的缓存层。两者配合使用,可以实现上一节提到的高效 缓存机制。关于两个模块的详细信息可以参考它们Nginx官网的wiki(memc wikisrcache wiki)页。

安装及配置

下面以LNMP环境介绍如何使用这两个模块构建缓存层。

因为Nginx并不支持模块动态加载,所以要安装新的模块,必须重新编译Nginx。首先下载两个模块(memc下载地址,srcache下载地址),另外,为了发挥出缓存的最大性能,建议将memcache的upstream配置为keep-alive,为了支持upstream的keep-alive需要同时安装http-upstream-keepalive-module。

将模块下载并解压到合适的目录,这里我Nginx使用的版本是1.0.4,与相关模块一起解压到了/home/www/download

然后就可以编译安装Nginx了,命令如下:

./configure --prefix=/usr/local/nginx \
--add-module=../memc-nginx-module \
--add-module=../srcache-nginx-module \
--add-module=../ngx_http_upstream_keepalive
make
make install

这里我将nginx安装到/usr/local/nginx下,你可以根据自己的需要更改安装路径,另外,我只列出了本文必要的configure命令,你也可以增加需要的configure选项。
然后需要对nginx进行配置,nginx默认主配置文件放在安装目录的conf下,例如我的主配置文件为/usr/local/nginx/conf/nginx.conf。
这里我只贴出相关的配置:

#Memcache服务upstream
upstream memcache {
    server localhost:11211;
    keepalive 512 single;
}
server {
    listen       80;
    server_name  localhost;
    #memc-nginx-module
    location /memc {
        internal;
        memc_connect_timeout 100ms;
        memc_send_timeout 100ms;
        memc_read_timeout 100ms;
        set $memc_key $query_string;
        set $memc_exptime 300;
        memc_pass memcache;
    }
    location / {
        root   /var/www;
        index  index.html index.htm index.php;
    }
    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    location ~ \.php$ {
        charset        utf-8;
        default_type   text/html;
        #srcache-nginx-module
        set $key $uri$args;
        srcache_fetch GET /memc $key;
        srcache_store PUT /memc $key;
        root           /var/www;
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        include        fastcgi_params;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

下面解释一下其中几个点。
上文说过,memc-nginx是一个标准的upstream模块,因此首先需要定义memcache的upstream。这里我在本机上启动了一个 memcache服务,端口为默认的11211,keepalive指令是http-upsteram-keepalive-module提供的功能,这 里我们最大保持512个不立即关闭的连接用于提升性能。

下面是为memc-nginx-module配置location,我们配置为/memc,所有请求都通过请求这个location来操作 memcache,memc-nginx-module存取memcache是基于http method语义的,使用http的GET方法表示get、PUT方法表示set、DELETE方法表示delete。这里我们将/memc设为internal表示只接受内部访问,不接收外部http请求,这是为了安全考虑,当然如果需要通过http协议开放外部访问,可以去掉internal然后使用deny和allow指 令控制权限。比较重要的是$memc_key这个变量,它表示以什么作为key,这里我们直接使用Nginx内置的$query_string来作为 key,$memc_exptime表示缓存失效时间,以秒记。这里统一设为300(5分钟),在实际应用中可以根据具体情况为不同的内容设置不同的过期 时间。

最后我们为“~ \.php$”这个location配置了缓存,这表示所有以“.php”结尾的请求都会结果被缓存,当然这里只是示例需要,实际中一般不会这么配,而是为特定需要缓存的location配置缓存。

srcache_fetch表示注册一个输入拦截处理器到location,这个配置将在location进入时被执行;而srcache_store表 示注册一个输出拦截器到location,当location执行完成并输出时会被执行。注意srcache模块实际可以与任何缓存模块进行配合使用,而 不必一定是memc。这里我们以$uri$args作为缓存的key。

经过上述配置后,相当于对Nginx增加了如下逻辑:当所请求的uri以“.php”结尾时,首先到memcache中查询有没有以$uri$args为 key的数据,如果有则直接返回;否则,执行location的逻辑,如果返回的http状态码为200,则在输出前以$uri$args为key,将输 入结果存入memcache。

更多配置

上一节给出了使用memc和srcache构建缓存层的最基本方法,实际应用中可能需要更多灵活的配置,例如为不同的location配置不同的缓存参 数,根据返回内容而不是返回的http状态码确定是否缓存等等。可以有很多的方法实现这些需求,例如,srcache还支持两个指 令:srcache_fetch_skip和srcache_fetch_skip,这两个指令接受一个参数,当参数已定义且非0时,则进行相应操作,否 则不进行。例如,如果配置了srcache_fetch_skip $skip,这条指令,那么只有当$skip的值为非0时,才将结果缓存,如果配合ngx_lua模块的set_by_lua指令,则可以实现复杂的缓存控制。如:

location /xxxx {
    set $key ...;
    set_by_lua $skip '
        if ngx.var.cookie_foo == "bar" then
            return 1
        end
        return 0
    ';
    srcache_fetch_skip $skip;
    srcache_store_skip $skip;
    srcache_fetch GET /memc $key;
    srcache_store GET /memc $key;
    # proxy_pass/fastcgi_pass/...
}

这表示对/xxxx这个location的访问,只有存在cookie “foo”且值为“bar”时缓存机制才起作用。关于ngx_lua的更多内容请参考其主页。

另外,我最近在春哥(章亦春在淘宝的昵称)的微博上看到他目前正在完善srcache的功能,为其实现更多RFC2616的缓存行为标准。关于这个模块的最新动态可以关注其github主页。

Benchmark

下面对使用memc和srcache构建的缓存机制进行一个简单的benchmark,并与使用PHP操作memcache的策略进行一个对比。为了简单起见,我们的测试PHP脚本不去访问I/O,而仅仅是调用phpinfo函数输出PHP相关信息。
测试一共分三组进行:第一组在Nginx和PHP中均不开启缓存,第二组仅使用PHP memcache缓存,第三组仅使用Nginx memcache缓存。三组都用ab程序去压,并发数为20,请求次数为10000。
这里的测试环境是我的一个虚拟机,操作系统为Ubuntu10,内存512M。Nginx采用epoll,单worker进程,memcache最大并发数为1024,最大使用内存64m。

不开启缓存

这一组我们不开启缓存,PHP程序非常简单:

<?php
phpinfo();
?>

测试结果如下:

PHP memcache缓存策略

第二组我们用PHP操作缓存,测试脚本为:

<?php
$memc = new Memcached;
$memc->addServer('localhost', 11211) or die('Connect to memcache server failed!');
$output = $memc->get('my_key');
if(empty($output)) {
    ob_start();
    phpinfo();
    $output = ob_get_contents();
    ob_end_clean();
    $memc->set('my_key', $output, 300);
}
echo $output; 
?>

测试结果如下:

Nginx memcache缓存策略

最后,我们将PHP脚本回归到不使用缓存的版本,并配置好memc和srcache缓存机制。
测试结果如下:

结果对比分析

为了直观,我取“每秒处理请求数”、“平均每个请求处理时间”和“吞吐率”作为评价指标,制作了一张图表。

我想看到图表,结论已毋需我多言。在各项指标上使用memc和srcache构建的缓存机制都大大优于使用PHP操作memcache。其中每秒处理请求数(并发度)和吞吐率都是其9倍左右,而平均个请求所用时间仅有传统策略的1/8。

这里要特别说明一下,这里之所以PHP memcache策略比不使用缓存优势不明显,是因为我们的PHP脚本不涉及I/O操作,如果其中存在如数据库存取,PHP memcache的优势还是有的,但不论如何,Nginx memcache策略在性能上的优势是其无法比拟的。

另外,除了性能优势外,使用这种策略还可以简化PHP逻辑,因为缓存这一层都放在Nginx中了,PHP就从缓存操作中解放了出来,因此是一举多得。

如果你的系统也构建在LNMP上(或LAMP)上,不妨使用本文提到的方法替代传统的缓存策略,尽情享受性能上的提升。

转载:张洋个人博客 --- 使用memc-nginx和srcache-nginx模块构建高效透明的缓存机制

来源:http://www.qixing318.com/article/using-memc-nginx-and-srcache-nginx-module-build-efficient-and-transparent-caching-mechanism.html

主流并发工具之ForkJoinPool

来源:http://wankunde.iteye.com/blog/1521042

ForkJoinPool 是 Java SE 7 新功能“分叉/结合框架”的核心类,现在可能乏人问津,但我觉得它迟早会成为主流。分叉/结合框架是一个比较特殊的线程池框架,专用于需要将一个任务不断分解成子任务(分叉),再不断进行汇总得到最终结果(结合)的计算过程。比起传统的线程池类ThreadPoolExecutorForkJoinPool 实现了工作窃取算法,使得空闲线程能够主动分担从别的线程分解出来的子任务,从而让所有的线程都尽可能处于饱满的工作状态,提高执行效率。

ForkJoinPool 提供了三类方法来调度子任务:

execute 系列异步执行指定的任务。invoke 和 invokeAll执行指定的任务,等待完成,返回结果。submit 系列异步执行指定的任务并立即返回一个 Future 对象。子任务由 ForkJoinTask 的实例来代表。它是一个抽象类,JDK 为我们提供了两个实现:RecursiveTask 和 RecursiveAction,分别用于需要和不需要返回计算结果的子任务。ForkJoinTask 提供了三个静态的 invokeAll 方法来调度子任务,注意只能在 ForkJoinPool 执行计算的过程中调用它们。ForkJoinPool 和 ForkJoinTask 还提供了很多让人眼花缭乱的公共方法,其实它们大多数都是其内部实现去调用的,对于应用开发人员来说意义不大。

下面以统计 D 盘文件个数为例。这实际上是对一个文件树的遍历,我们需要递归地统计每个目录下的文件数量,最后汇总,非常适合用分叉/结合框架来处理:

// 处理单个目录的任务
public class CountingTask extends RecursiveTask<Integer> {
private Path dir;

public CountingTask(Path dir) {
this.dir = dir;
}

@Override
protected Integer compute() {
int count = 0;
List<CountingTask> subTasks = new ArrayList<>();

// 读取目录 dir 的子路径。
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) {
for (Path subPath : ds) {
if (Files.isDirectory(subPath, LinkOption.NOFOLLOW_LINKS)) {
// 对每个子目录都新建一个子任务。
subTasks.add(new CountingTask(subPath));
} else {
// 遇到文件,则计数器增加 1。
count++;
}
}

if (!subTasks.isEmpty()) {
// 在当前的 ForkJoinPool 上调度所有的子任务。
for (CountingTask subTask : invokeAll(subTasks)) {
count += subTask.join();
}
}
} catch (IOException ex) {
return 0;
}
return count;
}
}

// 用一个 ForkJoinPool 实例调度“总任务”,然后敬请期待结果……
Integer count = new ForkJoinPool().invoke(new CountingTask(Paths.get("D:/")));

在我的笔记本上,经多次运行这段代码,耗费的时间稳定在 600 豪秒左右。普通线程池(Executors.newCachedThreadPool())耗时 1100 毫秒左右,足见工作窃取的优势。

结束本文前,我们来围观一个最神奇的结果:单线程算法(使用 Files.walkFileTree(...))比这两个都快,平均耗时 550 毫秒!这警告我们并非引入多线程就能优化性能,并须要先经过多次测试才能下结论。

实际测试

package com.forkjoin.countdir;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

import com.util.DateTime;

public class CountTaskRecursive extends RecursiveTask {

int sum =0;
File file ;

public CountTaskRecursive(File file) {
this.file = file;
}

@Override
protected Integer compute() {
Integer csum =0;
List<CountTaskRecursive> tasklist = new ArrayList<CountTaskRecursive>() ;
if(file.isDirectory())
{
for(File f:file.listFiles())
{
CountTaskRecursive t = new CountTaskRecursive(f);
tasklist.add(t);
}
}
else
csum ++;
if(!tasklist.isEmpty())
{
for(CountTaskRecursive t :invokeAll(tasklist))
csum += (Integer)t.join();
}
return csum;
}

public static void main(String[] args) {
DateTime dt = new DateTime() ;
System.out.println("系统日期:"+dt.getDate()) ;
System.out.println("中文日期:"+dt.getDateComplete()) ;
System.out.println("时间戳:"+dt.getTimeStamp()) ;
CountTaskRecursive task = new CountTaskRecursive(new File("F:\\私人资料"));
Integer sum = (Integer)new ForkJoinPool().invoke(task);
System.out.println(sum);
dt = new DateTime() ;
System.out.println("系统日期:"+dt.getDate()) ;
System.out.println("中文日期:"+dt.getDateComplete()) ;
System.out.println("时间戳:"+dt.getTimeStamp()) ;
}
}

package com.forkjoin.countdir;
import java.io.File;
import java.security.Timestamp;
import java.util.Calendar;

import com.util.DateTime;

public class CountTaskSingle {
static int sum = 0;

public void countDir(File file)
{
if(file.isDirectory())
{
for(File f:file.listFiles())
countDir(f);
}
else
sum++;

}

public static void main(String[] args) {
CountTaskSingle ins = new CountTaskSingle();
DateTime dt = new DateTime() ;
System.out.println("系统日期:"+dt.getDate()) ;
System.out.println("中文日期:"+dt.getDateComplete()) ;
System.out.println("时间戳:"+dt.getTimeStamp()) ;
ins.countDir(new File("F:\\私人资料"));
System.out.println(sum);
dt = new DateTime() ;
System.out.println("系统日期:"+dt.getDate()) ;
System.out.println("中文日期:"+dt.getDateComplete()) ;
System.out.println("时间戳:"+dt.getTimeStamp()) ;
}
}

package com.util;

import java.util.*; // 导入需要的工具包
public class DateTime { // 以后直接通过此类就可以取得日期时间
private Calendar calendar = null; // 声明一个Calendar对象,取得时间
public DateTime() { // 构造方法中直接实例化对象
this.calendar = new GregorianCalendar();
}
public String getDate() { // 得到的是一个日期:格式为:yyyy-MM-dd HH:mm:ss.SSS
// 考虑到程序要频繁修改字符串,所以使用StringBuffer提升性能
StringBuffer buf = new StringBuffer();
buf.append(calendar.get(Calendar.YEAR)).append("-"); // 增加年
buf.append(this.addZero(calendar.get(Calendar.MONTH) + 1, 2)).append("-"); // 增加月
buf.append(this.addZero(calendar.get(Calendar.DAY_OF_MONTH), 2)).append(" "); // 取得日
buf.append(this.addZero(calendar.get(Calendar.HOUR_OF_DAY), 2)).append(":"); // 取得时
buf.append(this.addZero(calendar.get(Calendar.MINUTE), 2)).append(":");
buf.append(this.addZero(calendar.get(Calendar.SECOND), 2)).append(".");
buf.append(this.addZero(calendar.get(Calendar.MILLISECOND), 3));
return buf.toString();
}
public String getDateComplete() { // 得到的是一个日期:格式为:yyyy年MM月dd日 HH时mm分ss秒SSS毫秒
// 考虑到程序要频繁修改字符串,所以使用StringBuffer提升性能
StringBuffer buf = new StringBuffer();
buf.append(calendar.get(Calendar.YEAR)).append("年"); // 增加年
buf.append(this.addZero(calendar.get(Calendar.MONTH) + 1, 2)).append("月"); // 增加月
buf.append(this.addZero(calendar.get(Calendar.DAY_OF_MONTH), 2)).append("日"); // 取得日
buf.append(this.addZero(calendar.get(Calendar.HOUR_OF_DAY), 2)).append("时"); // 取得时
buf.append(this.addZero(calendar.get(Calendar.MINUTE), 2)).append("分"); // 取得分
buf.append(this.addZero(calendar.get(Calendar.SECOND), 2)).append("秒"); // 取得秒
buf.append(this.addZero(calendar.get(Calendar.MILLISECOND), 3)).append("毫秒"); // 取得毫秒
return buf.toString();
}
public String getTimeStamp() { // 得到的是一个时间戳
// 考虑到程序要频繁修改字符串,所以使用StringBuffer提升性能
StringBuffer buf = new StringBuffer();
buf.append(calendar.get(Calendar.YEAR)); // 增加年
buf.append(this.addZero(calendar.get(Calendar.MONTH) + 1, 2)); // 增加月
buf.append(this.addZero(calendar.get(Calendar.DAY_OF_MONTH), 2)); // 取得日
buf.append(this.addZero(calendar.get(Calendar.HOUR_OF_DAY), 2)); // 取得时
buf.append(this.addZero(calendar.get(Calendar.MINUTE), 2)); // 取得分
buf.append(this.addZero(calendar.get(Calendar.SECOND), 2)); // 取得秒
buf.append(this.addZero(calendar.get(Calendar.MILLISECOND), 3)); // 取得毫秒
return buf.toString();
}
// 考虑到日期中存在前导0,所以在此处加上补零的方法
private String addZero(int num, int len) {
StringBuffer s = new StringBuffer();
s.append(num);
while (s.length() < len) { // 如果长度不足,则继续补0
s.insert(0, "0"); // 在第一个位置处补0
}
return s.toString();
}
};

执行结果:

单线程执行结果

系统日期:2012-05-09 23:59:07.468

中文日期:2012年05月09日23时59分07秒468毫秒

时间戳:20120509235907468

389578

系统日期:2012-05-09 23:59:56.421

中文日期:2012年05月09日23时59分56秒421毫秒

时间戳:20120509235956421

多线程执行结果

系统日期:2012-05-10 00:01:57.500

中文日期:2012年05月10日00时01分57秒500毫秒

时间戳:20120510000157500

389578

系统日期:2012-05-10 00:09:00.281

中文日期:2012年05月10日00时09分00秒281毫秒

时间戳:20120510000900281