SpringBoot实现RESTful与RestTemplate

title: SpringBoot+JPA多数据源(注解方式)
date: 2019-06-27
author: maxzhao
tags:
  - JAVA
  - SpringBoot
  - RESTful
  - RestTemplate
  - 编写接口测试类
categories:
  - SpringBoot

RESTful

介绍

REST:表现层状态转化(Representational State Transfer)

RESTful是一种软件设计风格,就是目前最流行的一种互联网软件架构。它结构清晰、符合标准、易于理解、扩展方便。

Spring 对 RESTful 风格的接口有着天然的支持。

什么是REST

REST(RepresentationalState Transfer)是Roy Fielding 提出的一个描述互联系统架构风格的名词。REST定义了一组体系架构原则,您可以根据这些原则设计以系统资源为中心的Web 服务,包括使用不同语言编写的客户端如何通过 HTTP处理和传输资源状态。

为什么称为 REST?Web本质上由各种各样的资源组成,资源由URI 唯一标识。浏览器(或者任何其它类似于浏览器的应用程序)将展示出该资源的一种表现方式,或者一种表现状态。如果用户在该页面中定向到指向其它资源的链接,则将访问该资源,并表现出它的状态。这意味着客户端应用程序随着每个资源表现状态的不同而发生状态转移,也即所谓REST。

附:REST定义RESTSOAP的比较

REST成熟度的四个层次

第一个层次(Level0)的Web 服务只是使用 HTTP 作为传输方式,实际上只是远程方法调用(RPC)的一种具体形 式。SOAP和 XML-RPC都属于此类。

第二个层次(Level1)的Web 服务引入了资源的概念。每个资源有对应的标识符和表达。

第三个层次(Level2)的Web 服务使用不同的 HTTP 方法来进行不同的操作,并且使用HTTP 状态码来表示不同的结果。如 HTTPGET 方法来获取资源,HTTPDELETE 方法来删除资源。

第四个层次(Level3)的Web 服务使用 HATEOAS。在资源的表达中包含了链接信息。客户端可以根据链接来发现可以执行的动作。

其中第三个层次建立了创建、读取、更新和删除(create,read, update, and delete,CRUD)操作与 HTTP方法之间的一对一映射。根据此映射:

(1)若要在服务器上创建资源,应该使用POST 方法。

(2)若要检索某个资源,应该使用GET 方法。

(3)若要更改资源状态或对其进行更新,应该使用PUT 方法。

(4)若要删除某个资源,应该使用DELETE 方法。

HTTP请求的方法

(1)GET:通过请求URI得到资源
(2)POST:用于添加新的内容
(3)PUT:用于修改某个内容,若不存在则添加
(4)DELETE:删除某个内容
(5)OPTIONS :询问可以执行哪些方法
(6)HEAD :类似于GET, 但是不返回body信息,用于检查对象是否存在,以及得到对象的元数据
(7)CONNECT :用于代理进行传输,如使用SSL
(8)TRACE:用于远程诊断服务器

HTTP请求的状态码

(1)成功Successful2xx:此类状态码标识客户端的请求被成功接收、理解并接受。常见如200(OK)、204(NoContent)。
(2)重定向Redirection3xx:这个类别的状态码标识用户代理要做出进一步的动作来完成请求。常见如301(MovedPermanently)、302(MovedTemprarily)。
(3)客户端错误Client Error 4xx:4xx类别的状态码是当客户端象是出错的时使用的。常见如400(BadRequest)、401(Unauthorized)、403(Forbidden)、404(NotFound)。
(4)服务器错误Server Error 5xx:响应状态码以5开头表示服务器知道自己出错或者没有能力执行请求。常见如500(InternalServer Error)、502(BadGateway)、504(GatewayTimeout)。

附:HTTP1.1的标准简介

接口中的注解

  1. @Controller:修饰class,用来创建处理http请求的对象
  2. @RestController:Spring4之后加入的注解,原来在@Controller中返回实体需要@ResponseBody来配合,如果直接用@RestController替代@Controller就不需要再配置@ResponseBody,默认返回实体格式。
  3. @RequestMapping:配置 url 映射
  4. @PostMapping: 这个是@RequestMapping+POST方法的简写
  5. @RequestHeader: 请求Header参数
  6. @PathVariable: URL路径参数,比如/{id}中的id参数
  7. @RequestParam: URL请求参数
  8. @RequestBody: 请求Body参数

RESTful架构有一些典型的设计误区,就是URI包含动词。因为”资源”表示一种实体,所以应该是名词,URI不应该有动词,动词应该放在HTTP协议中。 上面设计的API的URI中都是名词。(此句引用)

RESTful 测试类


@AutoConfigureMockMvc
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ApplicationTests {
    @Autowired
    private MockMvc mvc;

    @Test
    public void testUserController() throws Exception {
        // 测试UserController
        RequestBuilder request;

        // 1、get查一下user列表,应该为空
        request = get("/users/");
        MvcResult result = mvc.perform(request)
                .andExpect(status().isOk())
                .andReturn();
        String content = result.getResponse().getContentAsString();
        BaseResponse<List<User>> response = JacksonUtil.json2Bean(content, new TypeReference<BaseResponse<List<User>>>() {});
        assertThat(response.isSuccess(), is(true));
        assertThat(response.getMsg(), is("查询列表成功"));
        assertThat(((List) response.getData()).size(), is(0));

        // 2、post提交一个user
        request = post("/users/")
                .param("id", "1")
                .param("name", "测试大师")
                .param("age", "20");
        result = mvc.perform(request)
                .andExpect(status().isOk())
                .andReturn();
        content = result.getResponse().getContentAsString();
        BaseResponse<String> response1 = JacksonUtil.json2Bean(content, new TypeReference<BaseResponse<String>>() {});
        assertThat(response1.isSuccess(), is(true));
        assertThat(response1.getMsg(), is("新增成功"));

        // 3、get获取user列表,应该有刚才插入的数据
        request = get("/users/");
        result = mvc.perform(request)
                .andExpect(status().isOk())
                .andReturn();
        content = result.getResponse().getContentAsString();
        BaseResponse<List<User>> response2 = JacksonUtil.json2Bean(content, new TypeReference<BaseResponse<List<User>>>() {});
        assertThat(response2.isSuccess(), is(true));
        assertThat(response2.getMsg(), is("查询列表成功"));
        assertThat((response2.getData()).size(), is(1));

        // 4、put修改id为1的user
        request = put("/users/1")
                .param("name", "测试终极大师")
                .param("age", "30");
        result = mvc.perform(request)
                .andExpect(status().isOk())
                .andReturn();
        content = result.getResponse().getContentAsString();
        BaseResponse<String> response3 = JacksonUtil.json2Bean(content, new TypeReference<BaseResponse<String>>() {});
        assertThat(response3.isSuccess(), is(true));
        assertThat(response3.getMsg(), is("更新成功"));

        // 5、get一个id为1的user
        request = get("/users/1");
        result = mvc.perform(request)
                .andExpect(status().isOk())
                .andReturn();
        content = result.getResponse().getContentAsString();
        BaseResponse<User> response4 = JacksonUtil.json2Bean(content, new TypeReference<BaseResponse<User>>() {});
        assertThat(response4.isSuccess(), is(true));
        assertThat(response4.getMsg(), is("查询成功"));
        User user = response4.getData();
        assertThat(user.getId(), is(1L));
        assertThat(user.getName(), is("测试终极大师"));

        // 6、del删除id为1的user
        request = delete("/users/1");
        result = mvc.perform(request)
                .andExpect(status().isOk())
                .andReturn();
        content = result.getResponse().getContentAsString();
        BaseResponse<String> response5 = JacksonUtil.json2Bean(content, new TypeReference<BaseResponse<String>>() {});
        assertThat(response5.isSuccess(), is(true));
        assertThat(response5.getMsg(), is("删除成功"));

        // 7、get查一下user列表,应该为空
        request = get("/users/");
        result = mvc.perform(request)
                .andExpect(status().isOk())
                .andReturn();
        content = result.getResponse().getContentAsString();
        BaseResponse<List<User>> response6 = JacksonUtil.json2Bean(content, new TypeReference<BaseResponse<List<User>>>() {});
        assertThat(response6.isSuccess(), is(true));
        assertThat(response6.getMsg(), is("查询列表成功"));
        assertThat((response6.getData()).size(), is(0));
    }

}

RestTemplate

介绍

用于同步客户端HTTP访问的Spring scentral类。它简化了与httpserver的通信,并实施了RESTful原则。它处理HTTP连接,让应用程序代码提供url(带有可能的模板变量)并提取结果。(简化了发起HTTP请求以及处理响应的过程,并且支持REST。)

RestTemplate默认依赖JDK提供http连接的能力(HttpURLConnection),如果有需要的话也可以通过setRequestFactory方法替换为例如 Apache HttpComponents、Netty或OkHttp等其它HTTP library。

实现逻辑

参考https://www.xncoding.com/2017/07/06/spring/sb-restclient.html

不同的 HTTP 请求实现方式

需要引入依赖

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <!--<version>4.5.3</version>-->
</dependency>

URLConnection

String result= "";
        BufferedReaderin = null;
        try {
            String urlNameString= url +"?" + param;
            URL realUrl= new URL(urlNameString);
            // 打开和URL之间的连接
            URLConnectionconnection = realUrl.openConnection();
            // 设置通用的请求属性
            connection.setRequestProperty("accept","*/*");
            connection.setRequestProperty("connection","Keep-Alive");
            connection.setRequestProperty("user-agent",
                    "Mozilla/4.0(compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            // 建立实际的连接
            connection.connect();
            // 获取所有响应头字段
            Map<String,List<String>> map = connection.getHeaderFields();
            // 遍历所有的响应头字段
            for(String key : map.keySet()) {
                System.out.println(key+ "--->" + map.get(key));
            }
            // 定义 BufferedReader输入流来读取URL的响应
            in =new BufferedReader(newInputStreamReader(
                    connection.getInputStream()));
            String line;
            while ((line = in.readLine())!= null) {
                result += line;
            }
        } catch (Exception e) {
            …
        }
        // 使用finally块来关闭输入流
        finally{
         // 关闭流
        }

RestTemplate

ResponseEntity<SsoUrlPrm>result = restTemplate.getForEntity(requestPathUrl,SsoUrlPrm.class);  

参考1https://www.cnblogs.com/zhaoyan001/p/8442602.html

参考2https://blog.csdn.net/weixin_39986856/article/details/83018655

使用

直接使用方式很简单:

public class RestTemplateTest { public static void main(String[] args) {
    RestTemplate restT = new RestTemplate();
    //通过Jackson JSON processing library直接将返回值绑定到对象  
    Quote quote = restT.getForObject("http://gturnquist-quoters.cfapps.io/api/random", Quote.class);
    String quoteString = restT.getForObject("http://gturnquist-quoters.cfapps.io/api/random", String.class);
    System.out.println(quoteString);
}}

发送GET请求

// 1-getForObject()
User user1 = this.restTemplate.getForObject(uri, User.class);
// 2-getForEntity()
ResponseEntity<User> responseEntity1 = this.restTemplate.getForEntity(uri, User.class);HttpStatus statusCode = responseEntity1.getStatusCode();HttpHeaders header = responseEntity1.getHeaders();User user2 = responseEntity1.getBody();  
// 3-exchange()
RequestEntity requestEntity = RequestEntity.get(new URI(uri)).build();ResponseEntity<User> responseEntity2 = this.restTemplate.exchange(requestEntity, User.class);User user3 = responseEntity2.getBody();

发送POST请求

// 1-postForObject()
User user1 = this.restTemplate.postForObject(uri, user, User.class);
// 2-postForEntity()
ResponseEntity<User> responseEntity1 = this.restTemplate.postForEntity(uri, user, User.class);
// 3-exchange()
RequestEntity<User> requestEntity = RequestEntity
.post(new URI(uri))
    .body(user);
ResponseEntity<User> responseEntity2 = this.restTemplate.exchange(requestEntity, User.class);

设置HTTP Header

// 1-Content-TypeRequest
Entity<User> requestEntity = RequestEntity        
.post(new URI(uri))        
    .contentType(MediaType.APPLICATION_JSON)        
    .body(user);
// 2-AcceptRequest
Entity<User> requestEntity = RequestEntity
.post(new URI(uri))
    .accept(MediaType.APPLICATION_JSON)
    .body(user);
// 3-OtherRequest
Entity<User> requestEntity = RequestEntity
.post(new URI(uri))        
    .header("Authorization", "Basic " + base64Credentials)        
    .body(user);

捕获异常

捕获HttpServerErrorException

try {    
    responseEntity = restTemplate.exchange(requestEntity, String.class);} catch (HttpServerErrorException e) {
    // log error
}

自定义异常处理器

public class CustomErrorHandler extends DefaultResponseErrorHandler {    @Override    
    public void handleError(ClientHttpResponse response) throws IOException {
    // todo    
}}

然后设置下异常处理器:

@Configurationpublic class RestClientConfig {
    @Bean    
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setErrorHandler(new CustomErrorHandler());
        return restTemplate;
    }
}

配置类

创建HttpClientConfig类,设置连接池大小、超时时间、重试机制等。配置如下:

/**
 * - Supports both HTTP and HTTPS
 * - Uses a connection pool to re-use connections and save overhead of creating connections.
 * - Has a custom connection keep-alive strategy (to apply a default keep-alive if one isn't specified)
 * - Starts an idle connection monitor to continuously clean up stale connections.
 */
@Configuration
@EnableScheduling
public class HttpClientConfig {

    private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientConfig.class);

    @Resource
    private HttpClientProperties p;

    @Bean
    public PoolingHttpClientConnectionManager poolingConnectionManager() {
        SSLContextBuilder builder = new SSLContextBuilder();
        try {
            builder.loadTrustMaterial(null, new TrustStrategy() {
                public boolean isTrusted(X509Certificate[] arg0, String arg1) {
                    return true;
                }
            });
        } catch (NoSuchAlgorithmException | KeyStoreException e) {
            LOGGER.error("Pooling Connection Manager Initialisation failure because of " + e.getMessage(), e);
        }

        SSLConnectionSocketFactory sslsf = null;
        try {
            sslsf = new SSLConnectionSocketFactory(builder.build());
        } catch (KeyManagementException | NoSuchAlgorithmException e) {
            LOGGER.error("Pooling Connection Manager Initialisation failure because of " + e.getMessage(), e);
        }

        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder
                .<ConnectionSocketFactory>create()
                .register("https", sslsf)
                .register("http", new PlainConnectionSocketFactory())
                .build();

        PoolingHttpClientConnectionManager poolingConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
        poolingConnectionManager.setMaxTotal(p.getMaxTotalConnections());  //最大连接数
        poolingConnectionManager.setDefaultMaxPerRoute(p.getDefaultMaxPerRoute());  //同路由并发数
        return poolingConnectionManager;
    }

    @Bean
    public ConnectionKeepAliveStrategy connectionKeepAliveStrategy() {
        return new ConnectionKeepAliveStrategy() {
            @Override
            public long getKeepAliveDuration(HttpResponse response, HttpContext httpContext) {
                HeaderElementIterator it = new BasicHeaderElementIterator
                        (response.headerIterator(HTTP.CONN_KEEP_ALIVE));
                while (it.hasNext()) {
                    HeaderElement he = it.nextElement();
                    String param = he.getName();
                    String value = he.getValue();
                    if (value != null && param.equalsIgnoreCase("timeout")) {
                        return Long.parseLong(value) * 1000;
                    }
                }
                return p.getDefaultKeepAliveTimeMillis();
            }
        };
    }

    @Bean
    public CloseableHttpClient httpClient() {
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(p.getRequestTimeout())
                .setConnectTimeout(p.getConnectTimeout())
                .setSocketTimeout(p.getSocketTimeout()).build();

        return HttpClients.custom()
                .setDefaultRequestConfig(requestConfig)
                .setConnectionManager(poolingConnectionManager())
                .setKeepAliveStrategy(connectionKeepAliveStrategy())
                .setRetryHandler(new DefaultHttpRequestRetryHandler(3, true))  // 重试次数
                .build();
    }

    @Bean
    public Runnable idleConnectionMonitor(final PoolingHttpClientConnectionManager connectionManager) {
        return new Runnable() {
            @Override
            @Scheduled(fixedDelay = 10000)
            public void run() {
                try {
                    if (connectionManager != null) {
                        LOGGER.trace("run IdleConnectionMonitor - Closing expired and idle connections...");
                        connectionManager.closeExpiredConnections();
                        connectionManager.closeIdleConnections(p.getCloseIdleConnectionWaitTimeSecs(), TimeUnit.SECONDS);
                    } else {
                        LOGGER.trace("run IdleConnectionMonitor - Http Client Connection manager is not initialised");
                    }
                } catch (Exception e) {
                    LOGGER.error("run IdleConnectionMonitor - Exception occurred. msg={}, e={}", e.getMessage(), e);
                }
            }
        };
    }

    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setThreadNamePrefix("poolScheduler");
        scheduler.setPoolSize(50);
        return scheduler;
    }
}

然后再配置RestTemplateConfig类,使用之前配置好的CloseableHttpClient类注入,同时配置一些默认的消息转换器:

/**
 * RestTemplate客户端连接池配置
 */
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class RestTemplateConfig {

    @Resource
    private CloseableHttpClient httpClient;

    @Bean
    public RestTemplate restTemplate(MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) {
        RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory());

        List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
        StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(Charset.forName("utf-8"));
        messageConverters.add(stringHttpMessageConverter);
        messageConverters.add(jackson2HttpMessageConverter);
        restTemplate.setMessageConverters(messageConverters);

        return restTemplate;
    }

    @Bean
    public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory() {
        HttpComponentsClientHttpRequestFactory rf = new HttpComponentsClientHttpRequestFactory();
        rf.setHttpClient(httpClient);
        return rf;
    }

}

发送文件

MultiValueMap<String, Object> multiPartBody = new LinkedMultiValueMap<>();
multiPartBody.add("file", new ClassPathResource("/tmp/user.txt"));
RequestEntity<MultiValueMap<String, Object>> requestEntity = RequestEntity        
.post(uri)        
    .contentType(MediaType.MULTIPART_FORM_DATA)        
    .body(multiPartBody);

下载文件

// 小文件
RequestEntity requestEntity = RequestEntity.get(uri).build();ResponseEntity<byte[]> responseEntity = restTemplate.exchange(requestEntity, byte[].class);byte[] downloadContent = responseEntity.getBody();  
// 大文件  
ResponseExtractor<ResponseEntity<File>> responseExtractor = new ResponseExtractor<ResponseEntity<File>>() {    
    @Override    
    public ResponseEntity<File> extractData(ClientHttpResponse response) throws IOException {        
        File rcvFile = File.createTempFile("rcvFile", "zip"); 
        FileCopyUtils.copy(response.getBody(), new FileOutputStream(rcvFile));        
        return ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()).body(rcvFile);    
    }
};
File getFile = this.restTemplate.execute(targetUri, HttpMethod.GET, null, responseExtractor);

Service注入

@Servicepublic 
class DeviceService {
    @Resource    
    private RestTemplate restTemplate;
}

实际使用例子

// 开始推送消息logger.info("解绑成功后推送消息给对应的POS机");
LoginParam loginParam = new LoginParam();
loginParam.setUsername(managerInfo.getUsername());
loginParam.setPassword(managerInfo.getPassword());HttpBaseResponse r = restTemplate.postForObject(        
    p.getPosapiUrlPrefix() + "/notifyLogin", loginParam, HttpBaseResponse.class);
if (r.isSuccess()) {    
    logger.info("推送消息登录认证成功");
    String token = (String) r.getData(); 
    UnbindParam unbindParam = new UnbindParam();  
    unbindParam.setImei(pos.getImei());   
    unbindParam.setLocation(location);
    // 设置HTTP Header信息   
    URI uri;    
    try {       
        uri = new URI(p.getPosapiUrlPrefix() + "/notify/unbind");  
    } catch (URISyntaxException e) {   
        logger.error("URI构建失败", e);    
        return 1;    
    }  
    RequestEntity<UnbindParam> requestEntity = RequestEntity            .post(uri)  
        .contentType(MediaType.APPLICATION_JSON)     
        .accept(MediaType.APPLICATION_JSON)     
        .header("Authorization", token)     
        .body(unbindParam); 
    ResponseEntity<HttpBaseResponse> responseEntity = restTemplate.exchange(requestEntity, HttpBaseResponse.class); 
    HttpBaseResponse r2 = responseEntity.getBody();
    if (r2.isSuccess()) {      
        logger.info("推送消息解绑网点成功"); 
    } else {     
        logger.error("推送消息解绑网点失败,errmsg = " + r2.getMsg());  
    }
} else { 
    logger.error("推送消息登录认证失败");
}

本文地址:
SpringBoot实现RESTful与RestTemplate

推荐

JAVA自定义注解

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

推荐阅读更多精彩内容