SpringBoot快速开发Restful Api

Spring-Boot Restful Api

1、Restful API开发

1.1 Restful简介

springMVC对编写Restful Api提供了很好的支持。

Restful Api有三个主要的特性:

  • 是基于Http协议的,是无状态的。
  • 是以资源为导向的
  • 人性化的,返回体内部包含相关必要的指导和链接

面向资源?
传统的Api接口以动作为导向,并且请求方法单一。例如/user/query?id=1 GET方法 ;/user/create
POST方法 而在resultful风格下以资源为导向,例如: /user/id(GET方法,获取) /user/(POST方法,创建)

restful api 用url描述资源,用Http方法描述行为,用Http状态码描述不同的结果,使用json作为交互数据(包括入参和响应)
restful只是一种风格并不是一种强制的标准

1.2 编写restful api 测试用例

因为restful api 与传统api存在一些风格上的差异,例如以method代表行为。所以在开发的过程中需要一边开发一边测试,测试我们的接口是否达到了预期的目的。springBoot提供了开发restful api测试用例的方法。首先导入依赖

    <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
    </dependency>

1.3 编写restful接口

1.3.1 基本注解

  • @RestController 声明一个controller负责提供restful接口
  • @RequestMapping 将请求的url映射到方法
  • @RequetParam 映射请求参数到方法请求参数 可以指定required指定此参数是否必填,name参数指定别名,defaultValue指定默认值。在传参时,SpringMVC会自动封装参数,所以可以在方法中用一个对象参数接收

1.3.2 @PathVariable

映射url片段到java方法参数

    @GetMapping("/user/{id}")
    public User getUserInfo(@PathVariable("id") String id){
        return new User("sico","12345");
    }

1.3.3 在url声明中使用正则表达式

在@pathVariable中url片段默认可以接收任何格式,任何类型,可以用正则表达式加以限定,例如:

    /**
     * 获取用户详情,利用正则表达式限定为只接收数字
     * @param id
     * @return
     */
    @GetMapping("/user/{id:\\d+}")
    public User getUserInfo(@PathVariable("id") String id){
        return new User("sico","12345");
    }

1.3.4 使用@jsonView控制json输出内容

SpringMVC会将实体对象转换成json返回。有时候我们希望在不同的请求中隐藏一些字段。可以用@JsonView控制输出内容。
使用@jsonView注解有以下步骤:

  • 使用接口来声明多个视图
  • 在值对象的getter方法上指定视图
  • 在controller方法上指定视图

使用接口声明视图
此接口只作声明使用,可以直接放置到目标实体内部,示例:

public class User implements Serializable{

    public interface SimpleView{};
    public interface DetailView extends SimpleView{};
    //....
}

注意继承关系,DetailView继承了SimpleView。即视图DetailView会显示被SimpleView标注的视图

在值对象上的getter方法上指定视图

    @JsonView(SimpleView.class)
    public String getUsername() {
        return username;
    }
    //...
    @JsonView(DetailView.class)
    public String getPassword() {
        return password;
    }

在方法上指定视图

    /**
     * 获取用户详情,利用正则表达式限定为只接收数字
     * @param id
     * @return
     */
    @GetMapping("/user/{id:\\d+}")
    @JsonView(User.DetailView.class)
    public User getUserInfo(@PathVariable("id") String id){
        return new User("sico","12345");
    }

由于视图的继承关系,DetailView任然会显示被SimpleView标注的字段

1.3.5 RequestMapping的变体

RequestMapping有以下变体,他们分别对应了不同的请求方法

  • @GetMapping 对应GET方法
  • @PostMapping 对应POST方法
  • @PutMapping 对应PUT方法
  • @DeleteMapping 对应DELETE方法

1.3.5 @RequestBody将请求体映射到java方法参数

@(spring)RequestBody将请求中的请求体中的实体数据转换成实体对象,常用语PUT和POST

    /**
     * 创建用户
     * 仅有加入@RequestBody注解才能解析出请求体重传入的实体数据
     */
    @PutMapping("/user")
    public void create(@RequestBody User user){
        User user1=new User("cocoa","123",1);
    }

1.3.6 @Valid注解和BindingResult验证请求参数的合法性并处理校验结果

一般需要在请求接口中校验请求参数,例如参数是否为空,是否唯一等。

  • @NotBlack 非空注解,将此注解加到实体类属性上。
    @NotBlank
    private String username;

在请求方法的字段上加上@valid注解时,以上的注解将生效。如果请求接口的参数无法通过校验,将返回400

    @PutMapping("/user")
    public void create(@Valid @RequestBody User user){
        User user1=new User("cocoa","123",1);
    }

BindingResult
如果使用@valid注解,当参数不符合标准时。会直接返回400。而不会进入接口方法的方法体。如果需要对没通过校验的请求作一些处理。在使用BindingResult的情况下,如果用户传入的参数不符合约束。则相应的错误信息将会被放置在BindingRsult对象中。从BindingResult对象中取出错误信息:

    @PutMapping("/user")
    public void create(@Valid @RequestBody User user, BindingResult errors){
        if (errors.hasErrors()){
            errors.getAllErrors().stream().forEach(error->logger.error(error.getDefaultMessage()));
        }
        User user1=new User("cocoa","123",1);
    }

如果没有通过非空校验,将包含错误。默认的非空错误信息是:"may not be null",这个错误信息可以自定义。

1.3.6.1 hibernate validate常用校验注解
image

image
1.3.6.2 获取校验错误信息(包括字段信息)

使用fieldError可以获取错误的字段信息和错误信息

    @PutMapping("/user")
    public void update(@Valid @RequestBody User user,BindingResult errors){
        if (errors.hasErrors()){
            errors.getAllErrors().stream().forEach(error->{
                FieldError fieldError=(FieldError) error;
                String errorMessage=fieldError.getField()+" "+fieldError.getDefaultMessage();
                logger.error(errorMessage);
            });
        }
        User user1=new User("cocoa","123",1);
    }
1.3.6.3 自定义校验失败信息

用以上方式虽然能够获得错误字段和错误信息,但过于麻烦。可以在校验注解中指定message值自定义错误信息。如下:

    @NotBlank(message = "用户名不能为空")
    private String username;
1.3.6.3 自定义校验逻辑

默认的校验注解能够满足大部分的校验要求,但是依然不能完全满足要求。例如需要校验一个字段是否唯一,就无法通过默认的注解完成。此时需要自定义校验逻辑。可以通过自定义的注解来实现和自定义校验器实现

自定义注解

@Target({ElementType.FIELD,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
//java标准校验注解,validateBy指定校验的类
@Constraint(validatedBy = NameUniqueValidator.class )
public @interface NameUnique {

    //校验注解中必须实现以下三个属性
    String message() default "";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default { };
}

自定义校验器


/**
 * 实现ConstraintValidator接口,第一个泛型指定用于标注验证的注解,第二个泛型指定倍标注值得类型
 * 不需要用@component等注解将验证类加入容器。spring会自动将此类加入容器
 */
public class NameUniqueValidator implements ConstraintValidator<NameUnique,Object>{

    //在这个类中可以使用Spring @Autowire注解注入任何需要的对象

    /**
     * 校验器初始化
     * @param nameUnique
     */
    @Override
    public void initialize(NameUnique nameUnique) {

    }

    /**
     * 校验方法
     * @param o 待校验的值
     * @param constraintValidatorContext
     * @return 返回true代表校验成功,false代表校验失败
     */
    @Override
    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
        //TODO 执行校验逻辑
        return false;
    }
}

1.4 服务异常处理

1.4.1 SpringBoot默认的错误处理机制

SpringBoot会自动的处理一些异常。例如访问了一个不存在的页面,当使用浏览器访问时,SpringBoot会返回一个默认的错误页面,如下所示:


image

但使用postman访问时,返回如下错误信息:

{
    "timestamp": 1509626392183,
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/12ss"
}

原理:SpringBoot中包含一个BasicErrorController类用于处理错误,处理/error请求。当它检测到请求头中包含text/html的时候,返回一个错误的页面。当没有这个请求头时,返回json格式的错误。如何判断请求是否来自网页?使用注解:@RequestMapping(produces="text/html")
如下:


    @RequestMapping(
        produces = {"text/html"}
    )
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = this.getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
        return modelAndView == null ? new ModelAndView("error", model) : modelAndView;
    }

    @RequestMapping
    @ResponseBody
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = this.getStatus(request);
        return new ResponseEntity(body, status);
    }

可以模仿这种处理机制,在同一个url下做出不同的响应。

1.4.2 自定义异常处理

1.4.2 自定义返回的浏览器错误页面

自定义返回的浏览器错误页面只需要把相应的html文件放置在resources/resources/error文件夹下即可,404即404.html 500即500.html

1.4.3 自定义返回的json格式的错误信息

如果抛出自定义的异常,SpringBoot默认处理如下所示:

{
    "timestamp": 1509629240633,
    "status": 500,
    "error": "Internal Server Error",
    "exception": "com.sicosola.security.demo.exception.ServiceException",
    "message": "用户不存在",
    "path": "/user/1"
}

自定义异常返回格式
可以创建一个全局的控制器的错误处理器,从控制器抛出的异常都会在此处被拦截。可以在此处对它进行处理,首先自定义一个异常:


public class ServiceException extends RuntimeException{

    private Integer code;

    private String desc;

    public ServiceException(Integer code, String desc) {
        super(desc);
        this.code = code;
        this.desc = desc;
    }

    public Integer getCode() {
        return code;
    }

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

    public String getDesc() {
        return desc;
    }

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

定义一个controller全局异常处理器,处理异常

/**
 * 控制器错误处理器,从控制器抛出的异常被它拦截。
 * 可以在此处封装错误信息,以友好的方式返回给前端
 */

@ControllerAdvice
public class ControllerExceptionHandler {

    /**
     * 处理ServiceException
     * @return
     */
    @ExceptionHandler(ServiceException.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Map<String,Object> HandlerServiceException(ServiceException e){
        Map<String,Object> errorMessage=new HashMap<>();
        errorMessage.put("code",e.getCode());
        errorMessage.put("desc",e.getDesc());
        return errorMessage;
    }

}

1.5 Restful api拦截

一般来说,可以使用以下机制来拦截

  • 过滤器 (Filter)
  • 拦截器 (Interceptor)
  • 切片 (Aspect)

1.5.1 使用Filter

使用Filter仅需实现一个filter,将其加入容器即可。
在SpringBoot中,如何将不可更改源码的第三方Filter加入Spring容器中?
可以利用在配置类中利用FilterRegistrationBean将第三方过滤器注册到Spring

    /*
    用以下方式将第三方容器注册到Spring
     */
    @Bean
    public FilterRegistrationBean timeFilter(){
        FilterRegistrationBean registrationBean=new FilterRegistrationBean();
        //假设这是第三方容器
        TimeFilter filter=new TimeFilter();
        registrationBean.setFilter(filter);
        //可以声明这个filter在哪些路径起作用
        List<String> urls=new ArrayList<>();
        urls.add("/*");
        registrationBean.setUrlPatterns(urls);
        return registrationBean;
    }

使用Filter的缺陷
filter是由JavaEE提供的功能,它只能获取Http请求和Http响应的信息。无法知晓具体的业务是由某个控制器和某个方法完成的。

1.5.2 拦截器

拦截器是由Spring框架提供的功能,可以弥补Filter的不足

自定义Interceptor实现HandlerInterceptor.实现其处理方法后,在configuration中配置。
需要使配置类继承WebMvcConfigurerAdapter并覆盖其addInterceptors方法。

1.5.2.1 实现一个拦截器
    /**
 * 记录服务调用时间的拦截器
 */
@Component
public class TimeInterceptor implements HandlerInterceptor{

    private Logger logger= LoggerFactory.getLogger(getClass());

    /**
     * 处理前
     * @param httpServletRequest
     * @param httpServletResponse
     * @param handler 此参数记录了处理对象,包括类名和方法名等信息
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception {
        //设置开始时间
        httpServletRequest.setAttribute("startTime",new Date().getTime());
        //获取当前拦截接口处理类(Controller)
        logger.error(((HandlerMethod)handler).getBean().getClass().getName());
        //获取当前拦截接口的处理方法
        logger.error(((HandlerMethod)handler).getMethod().getName());
        //只有返回true才会执行后面的方法
        return true;
    }

    /**
     * 接口成功返回后,如果调用控制器方法时控制器方法抛出异常。则post方法不会被调用
     * @param httpServletRequest
     * @param httpServletResponse
     * @param o
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
        long startTime= (long) httpServletRequest.getAttribute("startTime");
        logger.error("TimeInterceptor耗时:"+(new Date().getTime()-startTime));
    }

    /**
     * 处理完成,无论控制器方法成功与否。都会进入这个方法
     * @param httpServletRequest
     * @param httpServletResponse
     * @param o
     * @param e
     * @throws Exception,当控制器方法抛出异常时,此exception有值,如果有全局异常处理器(参考ControllerExceptionHandler)它将拿不到异常对象
     */
    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
        long startTime= (long) httpServletRequest.getAttribute("startTime");
        logger.error("TimeInterceptor耗时:"+(new Date().getTime()-startTime));
    }
}
1.5.2.2 将拦截器注册到Spring
@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter{


    @Autowired
    TimeInterceptor timeInterceptor;

    /**
     * 此类继承自 WebMvcConfigureAdapter
      * @param registry 拦截器注册器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //将timeInterceptor注册
        registry.addInterceptor(timeInterceptor);
    }
}    

1.5.3 切面

拦截器能拦截请求并且能够获取到处理请求的控制器与方法。但是它依然无法拿到参数中的值。如果想获取参数的值,就需要使用切面。切片是Spring框架的核心功能之一。

要使用AOP,首先需要定义一个切面(切面中定义了处理的逻辑),此处声明一个切片名为TimeAspect。在声明一个切入点(切入点约定切片在哪些方法上起作用,在什么时候上起作用)
切入点常用的注解(约定在什么时候起作用)

  • @before 此注解约定切入点在目标方法执行前执行
  • @after 此注解约定切入点在目标方法执行后执行
  • @afterthrow 在方法抛出异常时调用
  • @around 覆盖了前3种,常用

在什么方法上起作用
在什么方法上起作用是用一个表达式指定的。

    //此注解声明类为切面
@Aspect
@Component
public class TimeAspect {

    private Logger logger= LoggerFactory.getLogger(getClass());

    /**
     * 定义切入点,
     * 第一个*表示任何返回值,第二个表示任何方法最后表示任何参数
     * @param joinPoint 此对象中包含了被切入方法的信息
     * @return
     */
    @Around("execution(* com.sicosola.security.demo.web.controller.UserController.*(..))")
    public Object handlerControllerMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        logger.error("Time aspect start !");
        //获取被切入方法的参数
        Object[] args = joinPoint.getArgs();
        for (Object arg:args){
            logger.error("args is "+arg);
        }
        long startTime=new Date().getTime();
        //执行目标方法,返回目标方法的返回值
        Object o = joinPoint.proceed();

        logger.error("耗时:"+(new Date().getTime()-startTime));
        return o;
    }
}

1.6 异步处理Rest服务

使用异步处理服务可以提高服务器的吞吐量,并且这种异步的处理对客户端是透明的。
在传统的同步模式下,所有的请求都在主线程中完成。Tomcat管理的线程是有最大数量的,当达到最大数量时。其它的请求就需要等待。而异步线程使用副线程,当请求发送到主线程时。主线程将任务交给副线程,主线程又可以继续接收请求。

1.6.1 使用Runable异步处理Rest服务

使用Callable单开一个线程执行任务,Callable是由java并发包提供的机制。

    @RequestMapping("/order")
    public Callable<String> Order() throws InterruptedException {
        logger.info("主线程开始");
        //使用Callable单开一个线程处理
        Callable<String> result=new Callable<String>() {
            @Override
            public String call() throws Exception {
                logger.info("处理线程开始");
                Thread.sleep(1000);
                logger.info("处理线程结束");
                return "success";

            }
        };

        Thread.sleep(1000);
        logger.info("主线程返回");
        return result;
    }

可以看到如下的日志:

2017-11-03 09:56:39.592  INFO 5148 --- [nio-8080-exec-1] c.s.s.demo.web.async.AsyncController     : 主线程开始
2017-11-03 09:56:40.592  INFO 5148 --- [nio-8080-exec-1] c.s.s.demo.web.async.AsyncController     : 主线程返回
2017-11-03 09:56:40.603  INFO 5148 --- [      MvcAsync1] c.s.s.demo.web.async.AsyncController     : 处理线程开始
2017-11-03 09:56:41.603  INFO 5148 --- [      MvcAsync1] c.s.s.demo.web.async.AsyncController     : 处理线程结束

可以看到处理业务实在副线程MvcAsync中打印出来的。根据日志可以看出,主线程几乎没有任何停顿就立即返回。

1.6.2 使用DeffrredResult异步处理Rest服务。

Runable并不能满足所有的场景,有时候可能使用消息队列在不同的服务器之间完成异步。使用Runable机制就不会有明显的效果。如下


image

此时需要使用DeffrredResult处理。它可以在两个不同的线程之间来传递。其大致处理流程如下:

  • 创建一个DeferredResultHolder
    @Component
public class DeferredResultHolder {

    //key代表订单号,value代表处理结果
    private Map<String,DeferredResult<String>> map=new HashMap<>();

    public Map<String, DeferredResult<String>> getMap() {
        return map;
    }

    public void setMap(Map<String, DeferredResult<String>> map) {
        this.map = map;
    }
}

  • 控制器方法接收到请求发送到消息队列,并创建一个DiferedResult,以订单号为key,result为value放到holder的map中。
  • 处理成功的消息监听器在收到处理结果后从holder中取出对应的DiferedResult并设置值。一旦该result被设置值就会异步返回。

最需要理解的是Holder,Holder只是作为一个容器保存了待接受值的所有diferredResult对象。Holder就作为两个不同线程之间的通信桥梁

1.6.3 异步处理配置

SpringWebMvcConfig中有个configureAsyncSupport方法,可以用此方法进行异步配置。可以在此配置类中注册异步拦截器,设置异步请求默认超时时间。设置自定义线程池。

    /**
     * 配置异步处理
     * @param configurer
     */
    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        //注册异步拦截器,此拦截器
        //configurer.registerCallableInterceptors();
        //configurer.registerDeferredResultInterceptors();
        //设置异步请求的默认超时时间
        configurer.setDefaultTimeout(10);
        //自定义线程池替代Spring默认的线程池
       // configurer.setTaskExecutor();
    }

2 SpringBoot中的配置信息封装

Spring Boot中一般会在resources目录下使用.properties文件或者.yml文件进行一些系统的配置。我们可以自定义自己的配置逻辑。自定义配置并在系统中读取配置。

首先利用@ConfigurationProperties(prefix="---")声明配置类,其中prefix是配置前缀。
然后利用@EnableConfigurationProperties使配置类起作用。参考:

public class BrowserProperties {

    private String loginPage;

    public String getLoginPage() {
        return loginPage;
    }

    public void setLoginPage(String loginPage) {
        this.loginPage = loginPage;
    }
}

/**
 * sico-security框架配置积累
 */

@ConfigurationProperties(prefix = "sico.security")
public class SecurityProperties {

    private BrowserProperties browser=new BrowserProperties();

    public BrowserProperties getBrowser() {
        return browser;
    }

    public void setBrowser(BrowserProperties browser) {
        this.browser = browser;
    }
}
@Configuration
//SecurityProperties配置读取器生效
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {

}

需要特别注意的是配置类中的属性名必须和配置项的名称完全相同,否则将无法正常读取

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,520评论 25 707
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,748评论 6 342
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,601评论 18 139
  • 看着击打在窗上的雨,我愣了。是什么时候起我开始学习忘记你,忘记埋藏在我心底最深的伤痕。 或许是那次的决绝,或许是那...
    离初夏阅读 116评论 0 0
  • 251)黄执中:别再茅坑争输赢 真正的沟通不是每次都能赢,而是避免让自己陷入需要努力沟通的情境。避免尴尬的方法是,...
    安的烈阅读 1,297评论 0 8