4

时间:2017年11月08日星期三
说明:使用JSR303规范校验http接口请求参数
源码:https://github.com/zccodere/s...

第一章:理论简介

1-1 背景介绍

如今互联网项目都采用HTTP接口形式进行开发。无论是Web调用还是智能设备APP调用,只要约定好参数形式和规则就能够协同开发。返回值用得最多的就是JSON形式。服务端除了保证正常的业务功能,还要经常对传进来的参数进行验证,例如某些参数不能为空,字符串必须含有可见字符,数值必须大于0等这样的要求。

1-2 基础理论

什么是JSR303规范

首先JSR 303是Java的标准规范。
根据官方文档的描述:在一个应用的不同层面(例如呈现层到持久层),
验证数据是一个是反复共同的任务。
许多时候相同的验证要在每一个独立的验证框架中出现很多次。
为了提升开发效率,阻止重复造轮子,于是形成了这样一套规范。
该规范定义了一个元数据模型,默认的元数据来源是注解(annotation)。

什么是AOP

是一种编程范式,不是编程语言
解决特定问题,不能解决所有问题
是OOP的补充,不是替代

JSR303定义的校验类型

空检查

@Null       验证对象是否为null
@NotNull    验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank   检查约束字符串是不是Null还有被Trim的长度是否大于0
@NotEmpty   检查约束元素是否为NULL或者是EMPTY.

Booelan检查

@AssertTrue     验证 Boolean 对象是否为 true  
@AssertFalse    验证 Boolean 对象是否为 false  

长度检查

@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内  
@Length(min=, max=) Validates that the annotated string is between min and max included.

日期检查

@Past       验证 Date 和 Calendar 对象是否在当前时间之前  
@Future     验证 Date 和 Calendar 对象是否在当前时间之后  
@Pattern    验证 String 对象是否符合正则表达式的规则

数值检查,建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为"",Integer为null

@Min            验证 Number 和 String 对象是否大等于指定的值  
@Max            验证 Number 和 String 对象是否小等于指定的值  
@DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
@DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
@Digits     验证 Number 和 String 的构成是否合法  
@Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。
@Range(min=, max=) 检查数字是否介于min和max之间.
@Range(min=10000,max=50000,message="range.bean.wage")

private BigDecimal wage;

@Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证)
@CreditCardNumber信用卡验证
@Email  验证是否是邮件地址,如果为null,不进行验证,算通过验证。
@ScriptAssert(lang= ,script=, alias=)
@URL(protocol=,host=, port=,regexp=, flags=)

相关jar包

validation-api-1.0.0.GA.jar:接口规范
hibernate-validator-4.2.0.Final.jar是对上述接口的实现

Gradle坐标

compile ('javax.validation:validation-api:1.1.0.Final')
compile ('org.hibernate:hibernate-validator:5.4.1.Final')

第二章:简单实战

2-1 基于SpringMVC

工程创建

创建名为valid-mvc的gradle工程build.gradle如下

apply plugin: 'war'
apply plugin: 'java'
apply plugin: 'eclipse'

[compileJava, javadoc, compileTestJava]*.options*.encoding = 'UTF-8'

ext {
    springVersion = "4.3.8.RELEASE"
}  

repositories {
    mavenLocal()
    maven{ url "http://maven.aliyun.com/nexus/content/groups/public" }
    mavenCentral()
}

dependencies {

    // Spring框架
    compile ("org.springframework:spring-core:${springVersion}")
    compile ("org.springframework:spring-beans:${springVersion}")
    compile ("org.springframework:spring-context:${springVersion}")
    compile ("org.springframework:spring-web:${springVersion}")
    compile ("org.springframework:spring-webmvc:${springVersion}")
    compile ("org.springframework:spring-aop:${springVersion}")    
    compile ("org.springframework:spring-aspects:${springVersion}")    
    
    compile ('javax.servlet:javax.servlet-api:3.0.1')
    compile ("com.alibaba:fastjson:1.2.20")
    
    // JSR303数据校验
    compile ('javax.validation:validation-api:1.1.0.Final')
    compile ('org.hibernate:hibernate-validator:5.4.1.Final')
}

代码编写

1.编写WebInitializer类

package com.zccoder.valid.mvc.config;

import java.util.EnumSet;

import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.servlet.DispatcherServlet;

/**
 * @Title apc-rest程序启动类
 * @Description 当web容器启动项目的时候执行
 * @author zc
 * @version 1.0 2017-11-08
 */
public class WebInitializer implements  WebApplicationInitializer  {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
      
        // 新建WebApplication,注册配置类,并将其和当前servletContext关联。
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.register(SpringConfig.class);
        context.setServletContext(servletContext);
        
        // 注册SpringMvc的DispatcherServlet。
        Dynamic servlet = servletContext.addServlet("dispatcher", new DispatcherServlet(context));
        servlet.addMapping("/");
        servlet.setLoadOnStartup(1);
        
        // 注册SpringMVC的字符过滤器
        FilterRegistration.Dynamic encodingFilter = servletContext.addFilter("encoding", new CharacterEncodingFilter());
        EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class);
        dispatcherTypes.add(DispatcherType.REQUEST);
        dispatcherTypes.add(DispatcherType.FORWARD);
        encodingFilter.addMappingForUrlPatterns(dispatcherTypes, true, "/*");
        encodingFilter.setInitParameter("encoding", "utf-8");
    }
}

2.编写SpringConfig类

package com.zccoder.valid.mvc.config;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;

/**
 * @Title spring配置文件类
 * @Description 配置spring的自动扫描
 * @author zc
 * @version 1.0 2017-11-08
 */
@Configuration
@EnableWebMvc
@EnableAspectJAutoProxy
@ComponentScan("com.zccoder.valid.mvc")
public class SpringConfig extends WebMvcConfigurerAdapter{
    
    /**
     * 配置FASTJSON
     */
    @Bean
    public FastJsonHttpMessageConverter fastJsonHttpMessageConverters() {
        FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();

        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setSerializerFeatures(SerializerFeature.QuoteFieldNames);
        fastJsonConfig.setCharset(Charset.forName("UTF-8"));
        fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");

        List<MediaType> supportedMediaTypes = new ArrayList<MediaType>();
        supportedMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);

        fastConverter.setSupportedMediaTypes(supportedMediaTypes);
        fastConverter.setFastJsonConfig(fastJsonConfig);
        return fastConverter;
    }
    
    /**
     * 配置JSON解析器
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        super.configureMessageConverters(converters);
        converters.add(this.fastJsonHttpMessageConverters());
    }
    
}

3.编写ValidAdvisor类

package com.zccoder.valid.mvc.config;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

import javax.validation.Valid;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;

import com.zccoder.valid.mvc.constants.EnRespStatus;
import com.zccoder.valid.mvc.vo.RspBaseVO;

/**
 * @Title JSR303数据校验切面
 * @Description 当校验不通过时,自动处理校验结果,停止当前请求,并响应错误提示
 * @author zc
 * @version 1.0 2017-11-08
 */
@Aspect
@Component
public class ValidAdvisor {

    // 通过@Pointcut注解声明切点。
    @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
    public void annotationPointCut() {
    };
    
    @Around("annotationPointCut()")
    public Object doTest(ProceedingJoinPoint pjp) throws Throwable{
        
        // 获取方法签名
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        Object[] args = pjp.getArgs();
        
        // 实例化方法返回值类型
        @SuppressWarnings("rawtypes")
        Class retuenClazz = signature.getReturnType();
        Object retuenObject = retuenClazz.newInstance();
        if(!(retuenObject instanceof RspBaseVO)){
            throw new RuntimeException("方法"+method.getName()+"返回值类型非法");
        }
        RspBaseVO rspVO = (RspBaseVO)retuenObject;
        
        // 遍历被拦截的方法上所有注解
        Annotation[][] annotations = method.getParameterAnnotations();  
        for(int i = 0; i < annotations.length; i++){  
            if(!hasValidAnnotation(annotations[i])){  
                continue;  
            }
            if(!(i < annotations.length-1 && args[i+1] instanceof BindingResult)){  
                //验证对象后面没有跟bindingResult,事实上如果没有应该到不了这一步  
                continue; 
            }
            BindingResult result = (BindingResult) args[i+1];  
            if(result.hasErrors()){
                FieldError fieldError = result.getFieldError();
                rspVO.setEnRespStatus(EnRespStatus.PARAM_INVALID);
                rspVO.setRespDesc(fieldError.getField() + fieldError.getDefaultMessage());
                return rspVO;
            }
        }
        
        return pjp.proceed();
    }
    
    /**
     * 校验是否有@Valid注解
     */
    private boolean hasValidAnnotation(Annotation[] annotations){  
        if(annotations == null){  
            return false;  
        }  
        for(Annotation annotation : annotations){  
            if(annotation instanceof Valid){  
                return true;  
            }  
        }
        return false;  
    }
}

4.编写EnRespStatus类

package com.zccoder.valid.mvc.constants;

public enum EnRespStatus {
    
    SUCCESS("0000","成功"),
    
    PARAM_INVALID("1000","缺少必传参数")
    ;
    
    private String respCode;
    
    private String respDesc;
    
    private EnRespStatus(String respCode,String respDesc){
        this.respCode = respCode;
        this.respDesc = respDesc;
    }

    public String getRespCode() {
        return respCode;
    }

    public String getRespDesc() {
        return respDesc;
    }
}

5.编写ReqBaseVO类

package com.zccoder.valid.mvc.vo;

import java.io.Serializable;

public class ReqBaseVO implements Serializable{
    
    private static final long serialVersionUID = 7023512707419434863L;

    private String transId;
    
    private String systemCall;
    
    @Override
    public String toString() {
        return "ReqBaseVO [transId=" + transId + ", systemCall=" + systemCall + "]";
    }

    public String getTransId() {
        return transId;
    }

    public void setTransId(String transId) {
        this.transId = transId;
    }

    public String getSystemCall() {
        return systemCall;
    }

    public void setSystemCall(String systemCall) {
        this.systemCall = systemCall;
    }

}

6.编写RspBaseVO类

package com.zccoder.valid.mvc.vo;

import java.io.Serializable;

import com.zccoder.valid.mvc.constants.EnRespStatus;

public class RspBaseVO implements Serializable{
    
    private static final long serialVersionUID = 7023512707419434863L;

    private String respCode;
    
    private String respDesc;

    @Override
    public String toString() {
        return "RspBaseVO [respCode=" + respCode + ", respDesc=" + respDesc + "]";
    }
    
    public void setEnRespStatus(EnRespStatus enRespStatus){
        this.respCode = enRespStatus.getRespCode();
        this.respDesc = enRespStatus.getRespDesc();
    }
    
    public String getRespCode() {
        return respCode;
    }

    public void setRespCode(String respCode) {
        this.respCode = respCode;
    }

    public String getRespDesc() {
        return respDesc;
    }

    public void setRespDesc(String respDesc) {
        this.respDesc = respDesc;
    }
    
    
    
}

7.编写StoreReqVO类

package com.zccoder.valid.mvc.vo;

import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.NotBlank;

public class StoreReqVO extends ReqBaseVO{

    private static final long serialVersionUID = -342969591273353836L;
    
    @Size(min=2,max=5)
    @NotBlank
    private String name;
    
    @NotBlank
    private String desc;

    @NotBlank
    private String code;
    
    @Override
    public String toString() {
        return "StoreVO [name=" + name + ", desc=" + desc + ", code=" + code + "]";
    }
    
    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
    
    
}

8.编写StoreRspVO类

package com.zccoder.valid.mvc.vo;

public class StoreRspVO extends RspBaseVO{

    private static final long serialVersionUID = -342969591273353836L;
    
    private String id;

    @Override
    public String toString() {
        return "StoreRspVO [id=" + id + "]";
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
    
    
    
}

9.编写StoreRest类

package com.zccoder.valid.mvc.rest;

import javax.validation.Valid;

import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.zccoder.valid.mvc.constants.EnRespStatus;
import com.zccoder.valid.mvc.vo.StoreReqVO;
import com.zccoder.valid.mvc.vo.StoreRspVO;

@RestController
@RequestMapping("/store")
public class StoreRest {
    
    @PostMapping("/create")
    public StoreRspVO create(@Valid StoreReqVO reqVO,BindingResult result){
        StoreRspVO rspVO = new StoreRspVO();
        
        System.out.println("注册成功:"+String.valueOf(reqVO.toString()));
        
        rspVO.setId(String.valueOf(System.currentTimeMillis()));
        rspVO.setEnRespStatus(EnRespStatus.SUCCESS);
        
        return rspVO;
    }
}

进行验证

启动项目,并使用Postman测试效果如下

clipboard.png

2-2 基于SpringBoot

工程创建

创建名为valid-boot的gradle工程build.gradle如下

// 使用SpringBoot插件
buildscript {
    ext {
        springBootVersion = '1.5.7.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

repositories {
    mavenLocal()
    maven{ url "http://maven.aliyun.com/nexus/content/groups/public" }
    mavenCentral()
}

dependencies {

    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.boot:spring-boot-starter-aop')
    
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

代码编写

1.编写ValidBootStart类

package com.zccoder.valid.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ValidBootStart {

    public static void main(String[] args) {
        SpringApplication.run(ValidBootStart.class, args);
    }
}

2.编写ValidAdvisor类

package com.zccoder.valid.boot.aspect;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

import javax.validation.Valid;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;

import com.zccoder.valid.boot.vo.EnRespStatus;
import com.zccoder.valid.boot.vo.RspBaseVO;

/**
 * @Title JSR303数据校验切面
 * @Description 当校验不通过时,自动处理校验结果,停止当前请求,并响应错误提示
 * @author zc
 * @version 1.0 2017-11-08
 */
@Aspect
@Component
public class ValidAdvisor {

    // 通过@Pointcut注解声明切点。
    @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
    public void annotationPointCut() {
    };
    
    @Around("annotationPointCut()")
    public Object doTest(ProceedingJoinPoint pjp) throws Throwable{
        
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        
        @SuppressWarnings("rawtypes")
        Class retuenClazz = signature.getReturnType();
        Object retuenObject = retuenClazz.newInstance();
        
        if(!(retuenObject instanceof RspBaseVO)){
            throw new RuntimeException("方法"+method.getName()+"返回值类型非法");
        }
        
        RspBaseVO rspVO = (RspBaseVO)retuenObject;
        
        Object[] args = pjp.getArgs();
        
        Annotation[][] annotations = method.getParameterAnnotations();  
        for(int i = 0; i < annotations.length; i++){  
            if(!hasValidAnnotation(annotations[i])){  
                continue;  
            }  
            if(!(i < annotations.length-1 && args[i+1] instanceof BindingResult)){  
                //验证对象后面没有跟bindingResult,事实上如果没有应该到不了这一步  
                continue; 
            }
            BindingResult result = (BindingResult) args[i+1];  
            if(result.hasErrors()){
                FieldError fieldError = result.getFieldError();
                rspVO.setEnRespStatus(EnRespStatus.PARAM_INVALID);
                rspVO.setRespDesc(fieldError.getField() + fieldError.getDefaultMessage());
                return rspVO;  
            }
        }
        return pjp.proceed();
    }
    
    /**
     * 校验是否有@Valid注解
     */
    private boolean hasValidAnnotation(Annotation[] annotations){  
        if(annotations == null){  
            return false;  
        }  
        for(Annotation annotation : annotations){  
            if(annotation instanceof Valid){  
                return true;  
            }  
        }  
        return false;  
    }
    
}

3.编写EnRespStatus类

package com.zccoder.valid.boot.vo;

public enum EnRespStatus {
    
    SUCCESS("0000","成功"),
    
    PARAM_INVALID("1000","缺少必传参数")
    ;
    
    private String respCode;
    
    private String respDesc;
    
    private EnRespStatus(String respCode,String respDesc){
        this.respCode = respCode;
        this.respDesc = respDesc;
    }

    public String getRespCode() {
        return respCode;
    }

    public String getRespDesc() {
        return respDesc;
    }
}

4.编写ReqBaseVO类

package com.zccoder.valid.boot.vo;

import java.io.Serializable;

public class ReqBaseVO implements Serializable{
    
    private static final long serialVersionUID = 7023512707419434863L;

    private String transId;
    
    private String systemCall;
    
    @Override
    public String toString() {
        return "ReqBaseVO [transId=" + transId + ", systemCall=" + systemCall + "]";
    }

    public String getTransId() {
        return transId;
    }

    public void setTransId(String transId) {
        this.transId = transId;
    }

    public String getSystemCall() {
        return systemCall;
    }

    public void setSystemCall(String systemCall) {
        this.systemCall = systemCall;
    }

}

5.编写RspBaseVO类

package com.zccoder.valid.boot.vo;

import java.io.Serializable;

public class RspBaseVO implements Serializable{
    
    private static final long serialVersionUID = 7023512707419434863L;

    private String respCode;
    
    private String respDesc;

    @Override
    public String toString() {
        return "RspBaseVO [respCode=" + respCode + ", respDesc=" + respDesc + "]";
    }
    
    public void setEnRespStatus(EnRespStatus enRespStatus){
        this.respCode = enRespStatus.getRespCode();
        this.respDesc = enRespStatus.getRespDesc();
    }
    
    public String getRespCode() {
        return respCode;
    }

    public void setRespCode(String respCode) {
        this.respCode = respCode;
    }

    public String getRespDesc() {
        return respDesc;
    }

    public void setRespDesc(String respDesc) {
        this.respDesc = respDesc;
    }
    
    
    
}

6.编写StoreReqVO类

package com.zccoder.valid.boot.vo;

import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.NotBlank;

public class StoreReqVO extends ReqBaseVO{

    private static final long serialVersionUID = -342969591273353836L;
    
    @Size(min=2,max=5)
    @NotBlank
    private String name;
    
    @NotBlank
    private String desc;

    @NotBlank
    private String code;
    
    @Override
    public String toString() {
        return "StoreVO [name=" + name + ", desc=" + desc + ", code=" + code + "]";
    }
    
    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
    
    
}

7.编写StoreRspVO类

package com.zccoder.valid.boot.vo;

public class StoreRspVO extends RspBaseVO{

    private static final long serialVersionUID = -342969591273353836L;
    
    private String id;

    @Override
    public String toString() {
        return "StoreRspVO [id=" + id + "]";
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
    
    
    
}

8.编写StoreRest类

package com.zccoder.valid.boot.rest;

import javax.validation.Valid;

import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.zccoder.valid.boot.vo.EnRespStatus;
import com.zccoder.valid.boot.vo.StoreReqVO;
import com.zccoder.valid.boot.vo.StoreRspVO;

@RestController
@RequestMapping("/store")
public class StoreRest {
    
    @PostMapping("/create")
    public StoreRspVO create(@Valid StoreReqVO reqVO,BindingResult result){
        StoreRspVO rspVO = new StoreRspVO();
        
        System.out.println("注册成功:"+String.valueOf(reqVO.toString()));
        
        rspVO.setId(String.valueOf(System.currentTimeMillis()));
        rspVO.setEnRespStatus(EnRespStatus.SUCCESS);
        
        return rspVO;
    }
}

进行验证

启动项目,并使用Postman测试效果如下

clipboard.png

参考资料
http://blog.csdn.net/chaijunk...
http://www.cnblogs.com/yangzh...


妙手空空
1.3k 声望368 粉丝

博观而约取,厚积而薄发