Spring boot学习(八)Spring boot配置ehcache缓存框架

前言

缓存技术在实际的项目中是必不可少的,合理的利用缓存技术能极大的提升网站的访问速度,提升用户体验。 本片文章就介绍如何在spring boot中使用ehcache这个缓存框架。

文章首发于个人博客:【http://www.xiongfrblog.cn

ehcache介绍

java中有很多技术都可以实现缓存功能,最简单直接就是使用java自带的Map容器,或者就是使用现有的缓存框架,例如memcache,ehcache ,以及非常热门的redis。这里介绍ehcache的主要是因为它真的很方便,而且memcacheredis都需要额外搭建服务,更适合分布式部署的项目以便于各个模块之间的使用共有的缓存内容。而ehcache主要是内存缓存,也可以缓存到磁盘中,速度快,效率高,功能也强大,适合我们一般的单个项目使用。

spring boot 配置ehcache

spring boot中配置ehcahce主要有以下四步:

  1. pom.xml中添加依赖
  2. 配置ehcache.xml配置文件
  3. 开启缓存
  4. 利用注解使用缓存

下面我们详细介绍每一步。

添加依赖

要想在spring boot中使用缓存,首先需要开启缓存,然后添加ehcache的依赖,所以我们在pom.xml中添加如下连个依赖项:

<!--开启缓存-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- EhCache -->
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>

编写配置文件

添加了依赖之后,spring boot会自动默认加载src/mian/resources目录下的ehcache.xml文件,所以我们需要在该目录下手动创建该文件,这里先给出一个样例:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
     
     <!-- 磁盘缓存文件路径 -->
     <diskStore path="java.io.tmpdir"/>
     
     <!-- 默认配置 -->
     <defaultCache eternal="false"
        maxElementsInMemory="1000"
        overflowToDisk="false"
        diskPersistent="false"
        timeToIdleSeconds="0"
        timeToLiveSeconds="600"
        memoryStoreEvictionPolicy="LRU"/>
        
     <!-- 自定义配置 -->
     <cache name="userCache"
       eternal="false"
       maxElementsInMemory="1000"
       overflowToDisk="false"
       diskPersistent="false"
       timeToIdleSeconds="0"
       timeToLiveSeconds="600"
       memoryStoreEvictionPolicy="LRU"/>
</ehcache>

下面介绍样例中出现的三个节点:

  • <diskStore>:这个节点是非必须的,只有在使用了磁盘存储的情况下才需要配置,表示缓存文件在磁盘中保存的路径,该路径通过path属性来指定,磁盘缓存使用的文件后缀名是*.data*.index,主要有以下几个值:
    1. user.home:用户主目录
    2. user.dir:用户当前的工作目录
    3. java.io.tmpdir:默认临时路径
    4. ehcache.disk.store.dir:cache的配置目录
    5. 自定义绝对路径

如果对于这几个目录不熟悉,可以在java中获取,如下:

public static void main(String[] args) {
    System.out.println(System.getProperty("user.home"));
    System.out.println(System.getProperty("user.dir"));
    System.out.println(System.getProperty("java.io.tmpdir"));
}

下面是我本机打印出来的路径,仅做参考:

C:\Users\Administrator
D:\Program Data\eclipse-workspace\springboot-ehcache
C:\Users\Administrator\AppData\Local\Temp\

这里需要注意一点,要想某个对象被缓存到磁盘中,需要该对象实现序列化接口。

  • <ehcache>:自定义缓存区,可以有零个或者多个,重要属性如下:

    • name:缓存区名字,必须属性,用来区分缓存区的唯一标识。

    • eternal:设置缓存区中的内容是否永久有效,可选值truefalse,如果选择true那么设置的timeToIdleSeconds以及timeToLiveSeconds将失效。

    • maxElementsInMemory:该缓存区中最多可以存放的对象数量,超过这个数量时,会根据overflowToDisk属性的值有不同的操作。

    • overflowToDisk:缓存对象超出最大数量时是否启用磁盘保存,可选值truefalse,值为true时,会将超出的内容缓存到磁盘中,为false时则会根据memoryStoreEvictionPolicy属性配置的策略替换掉原来的内容。

    • diskPersistent:磁盘存储是否在虚拟机重启后持续存在,默认是false,如果为true系统在初始化时会将磁盘中的内容加载到缓存。

    • timeToIdleSeconds:设置一个元素在过期前的空闲时间(单位:秒),即访问该元素的最大间隔时间,超过这个时间该元素就会被清除,默认值为0,表示一个元素可以无限的空闲。

    • timeToLiveSeconds:设置一个元素在缓存区中的生存时间(单位:秒),即从创建到清除的时间,超过这个时间,该元素就会被清除,默认值为0,表示一个元素可以无限的保存。

    • memoryStoreEvictionPolicy:缓存存储与清除策略。即达到maxElementsInMemory限制并且overflowToDisk值为falseehcache就会根据这个属性的值执行相应的清空策略,该属性有以下三个值分别代表ehcache的三种缓存清理策略,默认值为LRU

      1. FIFO:先进先出策略(First In First Out)
      2. LFU:最少被使用(Less Frequently Used),所有的缓存元素都会有一个属性记录该元素被使用的次数,清理元素时最小的那个将会被清除。
      3. LRU:最近最少使用(Least Resently Used),所有缓存的元素都会有一个属性记录最后一次使用的时间,清理元素时时间最早的那个元素将会被清除。
    • diskExpiryThreadIntervalSeconds:磁盘缓存的清理线程运行间隔,默认是120秒。

    • diskSpoolBufferSizeMB:设置磁盘缓存区的大小,默认为30MB。

    • maxEntriesLocalDisk:设置磁盘缓存区最多能存放元素的数量。

  • <defaultCache>:默认缓存区,即是一个name属性为default<ehcache>节点,属性和<ehcache>节点都一样,一个ehcache.xml文件中只能有一个<defaultCache>节点,当我们没有自定义的<ehcache>时,默认使用该缓存区。

对于defaultCache这里有需要注意的地方,因为他是一个特殊的<ehcache>,所以我们在自定义缓存区的时候不能再定义名为default的<ehcache>,并且在使用的时候也不能通过value=default来指定默认的缓存区。

这里补充一点,项目中如果不想使用默认的路径以及名字我们也可以自定义ehcache配置文件的名字以及路径,在application.properties配置文件中配置如下内容:

#后边的路径可以自己指定
spring.cache.ehcache.config=classpath:ehcache.xml

开启缓存

spring boot中开启缓存非常简单,只需要在在启动类上添加一个@EnableCaching注解即可。

使用注解

spring boot中使用ehcache缓存主要是通过注解来使用,而且我们一般在service实现层使用缓存功能,常用的注解如下:

@Cacheable

该注解主要用在方法上边,每当程序进入被该注解标记的方法时,系统会首先判断缓存中是否存在相同key的元素,如果存在就直接返回缓存区中存放的值,并且不会执行方法的内容,如果不存在就执行该方法,并且判断是否需要将返回值添加到缓存区中,常用属性:

  • value:指定使用哪个缓存区,就是我们在配置文件里边配置的<ehcache>节点的name属性对应的值,可以指定多个值。
    //指定一个
    @Cacheable(value="userCache")
    //指定多个
    @Cacheable(value={"userCache","userCache2"})
    
  • key:缓存元素的key,需要按照SpEL表达式编写,这个我们一般按照指定方法的参数来确定。
    //#p0表示将第一个参数当成key,也可以直接写参数名字例如:#id,两者表达意思一样
    @Cacheable(value="userCache",key="#p0")
    public SysUser getById(Integer id){//内容省略...};
    
  • condition:添加缓存的条件,需要按照SpEL表达式编写,仅当该属性返回true时才添加缓存。
     //仅当id>10时才缓存
     @Cacheable(value="userCache",key="#p0",condition="#p0>10")
     public SysUser getById(Integer id){//内容省略...};
    

@CachePut

该注解主要用在方法上边,能够根据方法的参数以及返回值以及自定义的条件判断是否添加缓存,该注解标记的方法一定会执行,其属性与@Cacheable一致。

@CachePut(value="userCache",key="#entity.id")
public SysUser insertSysuser(SysUser entity) {
    // TODO Auto-generated method stub      
    //省略内容
}

@CacheEvict

该注解主要用在方法上边,能根据条件对缓存进行清空,常用属性如下:

  • value:同上
  • key:同上
  • condition:同上
  • allEntries:是否清空所有缓存内容,默认为false,如果设置为true,那么在方法执行完成之后并且满足condition条件时会清空该缓存区的所有内容。
  • beforeInvocation:清除内容操作是否发生在方法执行之前,默认为false,表示清除操作在方法执行完之后再进行,如果方法执行过程中抛出异常,那么清除操作就不执行,如果为true,则表示在方法执行之前执行清除操作。
@CacheEvict(value="userCache",key="#p0",allEntries=false, beforeInvocation=true)
public int deleteByPrimarykey(Integer key) {
    // TODO Auto-generated method stub
    //省略内容
}

效果测试

上边介绍了spring boot配置ehcache的步骤,接下来测试缓存效果,本项目在整合了Mybatis以及日志框架的前提下进行,基本的代码就不贴出来了,直接给出最关键的service实现层以及controller的代码:

SysuserServiceImpl.java

package com.web.springbootehcache.service.impl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import com.web.springbootehcache.dao.SysUserMapper;
import com.web.springbootehcache.entity.SysUser;
import com.web.springbootehcache.service.IsysUserService;

/**
* @author Promise
* @createTime 2019年3月19日 
* @description
*/
@Service("sysuserService")
public class SysUserServiceImpl implements IsysUserService{
    
    private final static Logger log = LoggerFactory.getLogger(SysUserServiceImpl.class);
    
    @Autowired
    private SysUserMapper sysuserMapper;

    @Override
    @Cacheable(value="userCache",key="#p0")
    public SysUser fingByPrimarykey(Integer key) {
        // TODO Auto-generated method stub
        log.debug("去数据库查询了数据!");
        return sysuserMapper.selectByPrimaryKey(key);
    }

    @Override
    @CachePut(value="userCache",key="#p0.id")
    public SysUser updateSysuser(SysUser entity) {
        // TODO Auto-generated method stub
        log.debug("更新了数据库数据!");
        int res = sysuserMapper.updateByPrimaryKey(entity);
        if(res >0)
            return entity;
        else
            return null;
    }

    @Override
    @CachePut(value="userCache",key="#entity.id")
    public SysUser insertSysuser(SysUser entity) {
        // TODO Auto-generated method stub      
        int res = sysuserMapper.insert(entity);
        log.debug("新增了数据!id为:{}",entity.getId());
        if(res >0)
            return entity;
        else
            return null;
    }

    @Override
    @CacheEvict(value="userCache",key="#p0",beforeInvocation=true)
    public int deleteByPrimarykey(Integer key) {
        // TODO Auto-generated method stub
        log.debug("删除了数据!");
        return sysuserMapper.deleteByPrimaryKey(key);
    }

}

该类中给出了基本的CRUD操作对应的缓存操作,当然不是绝对的,实际使用中根据自己需要改动。

IndexController.java

package com.web.springbootehcache.controller;

import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.web.springbootehcache.entity.SysUser;
import com.web.springbootehcache.service.IsysUserService;

/**
* @author Promise
* @createTime 2019年3月19日
* @description
*/
@RestController
public class IndexController {
    
    private final static Logger log = LoggerFactory.getLogger(IndexController.class);
    
    @Autowired
    private IsysUserService sysuserService;

    @RequestMapping(value="/select/{id}")
    public Object index(@PathVariable Integer id) {
        Map<String, Object> map = new HashMap<>();
        SysUser sysuser = sysuserService.fingByPrimarykey(id);
        log.debug("查询了id为:{}的用户信息!",sysuser.getId());
        SysUser sysuser2 = sysuserService.fingByPrimarykey(id);
        log.debug("查询了id为:{}的用户信息!",sysuser2.getId());
        map.put("res", sysuser);
        return map;
    }
    
    @RequestMapping(value="/update")
    public Object update() {
        Map<String, Object> map = new HashMap<>();
        //第一次修改
        SysUser sysuser = new SysUser(1, "eran", "eran1", 20, "M");
        sysuserService.updateSysuser(sysuser);
        //第一次查询
        sysuser = sysuserService.fingByPrimarykey(1);
        log.debug("查询了id为:{}的用户信息!",sysuser.getId());
        //第2次修改
        sysuser = new SysUser(1, "eran", "eran2", 20, "M");
        sysuserService.updateSysuser(sysuser);
        //第2次查询
        sysuser = sysuserService.fingByPrimarykey(1);
        log.debug("查询了id为:{}的用户信息!",sysuser.getId());
        map.put("res", sysuser);
        return map;
    }
    
    @RequestMapping(value="/insert")
    public Object insert() {
        Map<String, Object> map = new HashMap<>();
        SysUser sysuser = new SysUser();
        sysuser.setName("admin");
        sysuser.setAge(22);
        sysuser.setPass("admin");
        sysuser.setSex("M");
        sysuserService.insertSysuser(sysuser);
        //查询
        sysuser = sysuserService.fingByPrimarykey(sysuser.getId());
        map.put("res", sysuser);
        return map;
    }
    
    @RequestMapping(value="/delete/{id}")
    public Object delete(@PathVariable Integer id) {
        Map<String, Object> map = new HashMap<>();
        sysuserService.deleteByPrimarykey(id);
        //查询
        SysUser sysuser = sysuserService.fingByPrimarykey(id);
        map.put("res", sysuser);
        return map;
    }
}

数据库测试数据

在这里插入图片描述

启动项目,访问localhost:1188/select/1,控制台日志如下:

预期效果:执行查询操作两次,访问数据库一次。

[default]2019-03-20 17:35:05,287 [http-nio-1188-exec-2 32] DEBUG >> 去数据库查询了数据! >> c.w.s.s.i.SysUserServiceImpl
[default]2019-03-20 17:35:05,327 [http-nio-1188-exec-2 110] INFO >> HikariPool-1 - Starting... >> c.z.h.HikariDataSource
[default]2019-03-20 17:35:05,332 [http-nio-1188-exec-2 68] WARN >> Registered driver with driverClassName=com.mysql.jdbc.Driver was not found, trying direct instantiation. >> c.z.h.u.DriverDataSource
[default]2019-03-20 17:35:06,106 [http-nio-1188-exec-2 123] INFO >> HikariPool-1 - Start completed. >> c.z.h.HikariDataSource
[default]2019-03-20 17:35:06,113 [http-nio-1188-exec-2 159] DEBUG >> ==>  Preparing: select id, `name`, pass, sex, age from sys_user where id = ?  >> c.w.s.d.S.selectByPrimaryKey
[default]2019-03-20 17:35:06,141 [http-nio-1188-exec-2 159] DEBUG >> ==> Parameters: 1(Integer) >> c.w.s.d.S.selectByPrimaryKey
[default]2019-03-20 17:35:06,176 [http-nio-1188-exec-2 159] DEBUG >> <==      Total: 1 >> c.w.s.d.S.selectByPrimaryKey
[default]2019-03-20 17:35:06,185 [http-nio-1188-exec-2 33] DEBUG >> 查询了id为:1的用户信息! >> c.w.s.c.IndexController
[default]2019-03-20 17:35:06,186 [http-nio-1188-exec-2 35] DEBUG >> 查询了id为:1的用户信息! >> c.w.s.c.IndexController

可以很直白的看出,我们执行了两次查询操作,但是从数据库中取数据的操作就执行了一次,可见还有一次直接从缓存中取数据,达到了我们预期的效果。

访问localhost:1188/update,代码中我们对id2的数据做了两次修改以及两次查询操作,并且在执行修改操作时缓存了数据,执行该方法之前,id2的数据还不在缓存中。

预期效果:执行两次修改操作,访问两次数据库,两次查询操作不访问数据库。

[default]2019-03-20 17:47:37,254 [http-nio-1188-exec-1 40] DEBUG >> 更新了数据库数据! >> c.w.s.s.i.SysUserServiceImpl
[default]2019-03-20 17:47:37,291 [http-nio-1188-exec-1 110] INFO >> HikariPool-1 - Starting... >> c.z.h.HikariDataSource
[default]2019-03-20 17:47:37,299 [http-nio-1188-exec-1 68] WARN >> Registered driver with driverClassName=com.mysql.jdbc.Driver was not found, trying direct instantiation. >> c.z.h.u.DriverDataSource
[default]2019-03-20 17:47:37,953 [http-nio-1188-exec-1 123] INFO >> HikariPool-1 - Start completed. >> c.z.h.HikariDataSource
[default]2019-03-20 17:47:37,964 [http-nio-1188-exec-1 159] DEBUG >> ==>  Preparing: update sys_user set `name` = ?, pass = ?, sex = ?, age = ? where id = ?  >> c.w.s.d.S.updateByPrimaryKey
[default]2019-03-20 17:47:38,006 [http-nio-1188-exec-1 159] DEBUG >> ==> Parameters: eran(String), eran1(String), M(String), 20(Integer), 2(Integer) >> c.w.s.d.S.updateByPrimaryKey
[default]2019-03-20 17:47:38,104 [http-nio-1188-exec-1 159] DEBUG >> <==    Updates: 1 >> c.w.s.d.S.updateByPrimaryKey
[default]2019-03-20 17:47:38,237 [http-nio-1188-exec-1 48] DEBUG >> 查询了id为:2的用户信息! >> c.w.s.c.IndexController
[default]2019-03-20 17:47:38,239 [http-nio-1188-exec-1 40] DEBUG >> 更新了数据库数据! >> c.w.s.s.i.SysUserServiceImpl
[default]2019-03-20 17:47:38,239 [http-nio-1188-exec-1 159] DEBUG >> ==>  Preparing: update sys_user set `name` = ?, pass = ?, sex = ?, age = ? where id = ?  >> c.w.s.d.S.updateByPrimaryKey
[default]2019-03-20 17:47:38,243 [http-nio-1188-exec-1 159] DEBUG >> ==> Parameters: eran(String), eran2(String), M(String), 20(Integer), 2(Integer) >> c.w.s.d.S.updateByPrimaryKey
[default]2019-03-20 17:47:38,286 [http-nio-1188-exec-1 159] DEBUG >> <==    Updates: 1 >> c.w.s.d.S.updateByPrimaryKey
[default]2019-03-20 17:47:38,287 [http-nio-1188-exec-1 54] DEBUG >> 查询了id为:2的用户信息! >> c.w.s.c.IndexController

结果符合我们预期。

新增操作和更新操作原理一样都是使用@CachePut注解,这里就不重复演示,直接测试删除数据清除相应缓存功能,访问localhost:1188/delete/2,此时缓存区中有id1,2的两条数据,我们删除id2的数据,再做查询操作。

预期效果:删除数据访问数据库一次,并清除缓存区中那个相应的数据,因为清除了缓存区的内容所以查询数据会访问数据库一次,但是数据库中相应的内容也已经被删除,所以查询不到任何数据。

[default]2019-03-20 17:57:28,337 [http-nio-1188-exec-4 64] DEBUG >> 删除了数据! >> c.w.s.s.i.SysUserServiceImpl
[default]2019-03-20 17:57:28,341 [http-nio-1188-exec-4 159] DEBUG >> ==>  Preparing: delete from sys_user where id = ?  >> c.w.s.d.S.deleteByPrimaryKey
[default]2019-03-20 17:57:28,342 [http-nio-1188-exec-4 159] DEBUG >> ==> Parameters: 2(Integer) >> c.w.s.d.S.deleteByPrimaryKey
[default]2019-03-20 17:57:28,463 [http-nio-1188-exec-4 159] DEBUG >> <==    Updates: 1 >> c.w.s.d.S.deleteByPrimaryKey
[default]2019-03-20 17:57:28,464 [http-nio-1188-exec-4 32] DEBUG >> 去数据库查询了数据! >> c.w.s.s.i.SysUserServiceImpl
[default]2019-03-20 17:57:28,467 [http-nio-1188-exec-4 159] DEBUG >> ==>  Preparing: select id, `name`, pass, sex, age from sys_user where id = ?  >> c.w.s.d.S.selectByPrimaryKey
[default]2019-03-20 17:57:28,468 [http-nio-1188-exec-4 159] DEBUG >> ==> Parameters: 2(Integer) >> c.w.s.d.S.selectByPrimaryKey
[default]2019-03-20 17:57:28,494 [http-nio-1188-exec-4 159] DEBUG >> <==      Total: 0 >> c.w.s.d.S.selectByPrimaryKey

日志输出的内容符合我们预期。

结语

好了,spring boot 整合ehcache的内容就到此为止了,下篇博客再见,bye~~

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

推荐阅读更多精彩内容