Freemarker

新的聊天

用户:

你知道Freemarker么?

ChatGPT:

是的,我了解FreeMarker。FreeMarker是一个流行的Java模板引擎,主要用于生成HTML页面、邮件、配置文件等文本输出。以下是FreeMarker的主要特点和基本用法:

主要特点:

  1. 模板语言简单易学
  2. 支持宏定义和嵌套模板
  3. 支持复杂的数据类型(List、Map等)
  4. 内置大量函数和指令
  5. 可以与主流Java框架集成(Spring等)

基本语法:

  1. 插值表达式: ${...}
Hello ${user.name}!
  1. FTL指令: #开头
<#if condition>
  ...
</#if>

<#list items as item>
  ${item}
</#list>
  1. 注释:
<#-- 这是注释 -->

常用指令:

  1. 条件判断:
<#if condition>
  ...
<#elseif condition2>
  ...
<#else>
  ...
</#if>
  1. 循环:
<#list users as user>
  ${user.name}
</#list>
  1. 包含其他模板:
<#include "header.ftl">
  1. 宏定义:
<#macro greet name>
  Hello ${name}!
</#macro>

<@greet name="John"/>

在Java中使用:

// 配置
Configuration cfg = new Configuration(Configuration.VERSION_2_3_29);
cfg.setDirectoryForTemplateLoading(new File("/templates"));

// 获取模板
Template temp = cfg.getTemplate("test.ftl");

// 准备数据
Map<String, Object> data = new HashMap<>();
data.put("user", user);

// 输出
Writer out = new FileWriter("output.html");
temp.process(data, out);

常见用途:

  1. 生成HTML页面
  2. 生成邮件内容
  3. 生成代码文件
  4. 生成配置文件
  5. 生成报表

注意事项:

  1. 注意模板文件的编码
  2. 处理null值
  3. 注意数字格式化
  4. 注意日期格式化
  5. 模板缓存的使用

这些是FreeMarker的基本概念和用法,它是一个功能强大且灵活的模板引擎。

用户:

有人说用这个可以做一些代码生成器?

ChatGPT:

是的,FreeMarker非常适合用来做代码生成器。它的主要优势在于:

  1. 模板语法灵活,可以处理复杂的代码生成逻辑
  2. 可以很好地处理各种数据类型
  3. 支持宏定义,可以复用代码模板
  4. 输出格式可控

这里是一个简单的代码生成器示例:

  1. 首先创建一个实体类模板(entity.ftl):
package ${packageName}.entity;

import lombok.Data;
import java.util.Date;

/**
 * ${tableComment}
 */
@Data
public class ${className} {
    <#list fields as field>
    /**
     * ${field.comment}
     */
    private ${field.type} ${field.name};
    
    </#list>
}
  1. 创建一个Service接口模板(service.ftl):
package ${packageName}.service;

import ${packageName}.entity.${className};

public interface ${className}Service {
    void save(${className} ${className?uncap_first});
    
    ${className} getById(Long id);
    
    // ... 其他方法
}
  1. Java代码生成器的核心实现:
public class CodeGenerator {
    private Configuration cfg;
    
    public void init() throws IOException {
        cfg = new Configuration(Configuration.VERSION_2_3_29);
        cfg.setDirectoryForTemplateLoading(new File("templates"));
        cfg.setDefaultEncoding("UTF-8");
    }
    
    public void generateCode(TableInfo tableInfo) throws Exception {
        // 准备数据模型
        Map<String, Object> data = new HashMap<>();
        data.put("packageName", "com.example");
        data.put("className", tableInfo.getClassName());
        data.put("tableComment", tableInfo.getComment());
        data.put("fields", tableInfo.getFields());
        
        // 生成实体类
        Template entityTemplate = cfg.getTemplate("entity.ftl");
        generateFile(entityTemplate, data, 
            "entity/" + tableInfo.getClassName() + ".java");
            
        // 生成Service接口
        Template serviceTemplate = cfg.getTemplate("service.ftl");
        generateFile(serviceTemplate, data, 
            "service/" + tableInfo.getClassName() + "Service.java");
    }
    
    private void generateFile(Template template, Map<String, Object> data, 
                            String outputPath) throws Exception {
        File file = new File("output/" + outputPath);
        file.getParentFile().mkdirs();
        
        try (Writer writer = new FileWriter(file)) {
            template.process(data, writer);
        }
    }
}
  1. 数据模型类:
@Data
public class TableInfo {
    private String className;
    private String comment;
    private List<FieldInfo> fields;
}

@Data
public class FieldInfo {
    private String name;
    private String type;
    private String comment;
}
  1. 使用示例:
public class Main {
    public static void main(String[] args) throws Exception {
        CodeGenerator generator = new CodeGenerator();
        generator.init();
        
        // 构建表信息
        TableInfo tableInfo = new TableInfo();
        tableInfo.setClassName("User");
        tableInfo.setComment("用户信息表");
        
        List<FieldInfo> fields = new ArrayList<>();
        fields.add(new FieldInfo("id", "Long", "主键ID"));
        fields.add(new FieldInfo("username", "String", "用户名"));
        fields.add(new FieldInfo("createTime", "Date", "创建时间"));
        tableInfo.setFields(fields);
        
        // 生成代码
        generator.generateCode(tableInfo);
    }
}

实际应用中,你可以:

  1. 从数据库中读取表结构信息
public TableInfo getTableInfo(Connection conn, String tableName) {
    // 读取表结构信息
    // 转换为TableInfo对象
}
  1. 添加更多的模板:
  • Controller模板
  • Mapper模板
  • Vue页面模板
  • 等等
  1. 增加自定义配置:
  • 包名配置
  • 输出路径配置
  • 模板路径配置
  • 文件覆盖策略
  1. 支持更多功能:
  • 字段类型映射
  • 命名规则转换
  • 模板参数配置
  • 代码格式化

这样的代码生成器可以:

  1. 提高开发效率
  2. 保证代码规范统一
  3. 减少重复工作
  4. 降低出错可能

实际项目中,很多框架都提供了类似的代码生成器,比如MyBatis-Plus的代码生成器。但是使用FreeMarker自定义代码生成器可以更灵活地满足特定项目的需求。

用户:

这个和PHP里面的一些常用的模版引擎技术有什么不同么,记得有一个smarty模版,还有一些其他类似的,反正记得就是写html,里面的变量都用特定的符号标识,也支持一些运算表达式进行逻辑运算或者循环,vue里面也有类似的。这些是同一种技术么?

ChatGPT:

你说得对,这些确实都是模板引擎技术,它们的基本原理和目标是相似的,但是在具体实现和应用场景上有一些区别:

  1. Smarty (PHP)
{* 这是注释 *}
Hello {$name}
{foreach $users as $user}
    {$user.name}
{/foreach}
  • PHP专用
  • 主要用于Web页面模板
  • 有缓存机制
  • 语法偏PHP风格
  1. FreeMarker (Java)
<#-- 这是注释 -->
Hello ${name}
<#list users as user>
    ${user.name}
</#list>
  • Java专用
  • 不仅用于Web页面,还常用于代码生成
  • 语法偏Java风格
  1. Vue模板
<!-- 这是注释 -->
Hello {{name}}
<div v-for="user in users">
    {{user.name}}
</div>
  • 前端框架的模板系统
  • 响应式数据绑定
  • 专注于UI组件
  • 支持虚拟DOM
  1. Thymeleaf (Java)
<!-- 这是注释 -->
<p th:text="${name}">Hello</p>
<div th:each="user : ${users}">
    <span th:text="${user.name}"></span>
</div>
  • Java专用
  • 可以直接在浏览器中预览
  • 与Spring框架紧密集成

主要区别:

  1. 使用场景
  • Smarty/Thymeleaf: 主要用于Web页面渲染
  • FreeMarker: Web页面渲染+代码生成
  • Vue模板: 前端组件开发
  1. 执行时机
  • Smarty/FreeMarker/Thymeleaf: 服务端渲染
  • Vue模板: 客户端渲染
  1. 功能特点
  • Smarty: 强大的缓存机制
  • FreeMarker: 强大的文本处理能力
  • Vue: 响应式数据绑定
  • Thymeleaf: 自然模板
  1. 性能特点
  • Smarty: 有缓存,性能好
  • FreeMarker: 编译型,性能稳定
  • Vue: 虚拟DOM,局部更新
  • Thymeleaf: 相对较慢

总的来说:

  1. 它们都是模板引擎技术
  2. 基本原理类似:
    • 定义模板语法
    • 提供数据模型
    • 模板+数据=最终输出
  3. 主要区别在于:
    • 使用的编程语言环境
    • 针对的具体应用场景
    • 实现的具体特性
    • 性能和使用方式

选择哪个主要看:

  1. 使用的开发语言
  2. 项目的具体需求
  3. 团队的技术栈
  4. 性能要求
  5. 开发效率需求

用户:

如果go语言需要做代码生成器,需要怎么实现?

ChatGPT:

Go语言实现代码生成器有几种常见方式:

  1. 使用内置的 text/template 包
    这是最基本的方式,Go标准库自带:
// 模板定义
const entityTmpl = `
package {{.PackageName}}

type {{.ClassName}} struct {
    {{range .Fields}}
    {{.Name}} {{.Type}} ` + "`json:\"{{.JsonName}}\"`" + `
    {{end}}
}
`

// 数据结构
type Field struct {
    Name     string
    Type     string
    JsonName string
}

type TemplateData struct {
    PackageName string
    ClassName   string
    Fields      []Field
}

// 生成代码
func generateCode() error {
    // 准备数据
    data := TemplateData{
        PackageName: "models",
        ClassName:   "User",
        Fields: []Field{
            {Name: "ID", Type: "int64", JsonName: "id"},
            {Name: "Name", Type: "string", JsonName: "name"},
        },
    }

    // 解析模板
    tmpl, err := template.New("entity").Parse(entityTmpl)
    if err != nil {
        return err
    }

    // 创建输出文件
    file, err := os.Create("user.go")
    if err != nil {
        return err
    }
    defer file.Close()

    // 执行模板
    return tmpl.Execute(file, data)
}
  1. 使用 jennifer 库
    Jennifer是一个更强大的代码生成库,可以通过API生成代码:
package main

import (
    "github.com/dave/jennifer/jen"
)

func main() {
    f := jen.NewFile("models")
    
    // 生成结构体
    f.Type().Id("User").Struct(
        jen.Id("ID").Int64().Tag(map[string]string{"json": "id"}),
        jen.Id("Name").String().Tag(map[string]string{"json": "name"}),
    )
    
    // 生成方法
    f.Func().Params(
        jen.Id("u").Op("*").Id("User"),
    ).Id("GetName").Params().String().Block(
        jen.Return(jen.Id("u").Dot("Name")),
    )
    
    // 输出到文件
    f.Save("user.go")
}
  1. 完整的代码生成器示例:
package main

import (
    "database/sql"
    "fmt"
    "os"
    "text/template"
    
    _ "github.com/go-sql-driver/mysql"
)

// 数据库表信息
type TableInfo struct {
    TableName    string
    StructName   string
    Columns      []ColumnInfo
}

type ColumnInfo struct {
    ColumnName string
    DataType   string
    Comment    string
}

// 模板
const tmpl = `
package {{.PackageName}}

// {{.StructName}} represents {{.TableName}} table structure
type {{.StructName}} struct {
    {{range .Columns}}
    {{.FieldName}} {{.GoType}} ` + "`json:\"{{.JsonName}}\" db:\"{{.ColumnName}}\"`" + ` // {{.Comment}}
    {{end}}
}
`

type Generator struct {
    db *sql.DB
}

func NewGenerator(dsn string) (*Generator, error) {
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        return nil, err
    }
    return &Generator{db: db}, nil
}

// 获取表结构信息
func (g *Generator) GetTableInfo(tableName string) (*TableInfo, error) {
    query := `
        SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT 
        FROM INFORMATION_SCHEMA.COLUMNS 
        WHERE TABLE_NAME = ? AND TABLE_SCHEMA = DATABASE()
    `
    
    rows, err := g.db.Query(query, tableName)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    info := &TableInfo{
        TableName:  tableName,
        StructName: toUpperCamelCase(tableName), // 需要实现这个函数
    }

    for rows.Next() {
        var col ColumnInfo
        err := rows.Scan(&col.ColumnName, &col.DataType, &col.Comment)
        if err != nil {
            return nil, err
        }
        info.Columns = append(info.Columns, col)
    }

    return info, nil
}

// 生成代码
func (g *Generator) Generate(tableName, outputPath string) error {
    // 获取表信息
    tableInfo, err := g.GetTableInfo(tableName)
    if err != nil {
        return err
    }

    // 解析模板
    tmpl, err := template.New("entity").Parse(tmpl)
    if err != nil {
        return err
    }

    // 创建输出文件
    file, err := os.Create(outputPath)
    if err != nil {
        return err
    }
    defer file.Close()

    // 执行模板
    return tmpl.Execute(file, tableInfo)
}

func main() {
    dsn := "user:password@tcp(localhost:3306)/dbname"
    gen, err := NewGenerator(dsn)
    if err != nil {
        panic(err)
    }

    err = gen.Generate("users", "models/user.go")
    if err != nil {
        panic(err)
    }
}
  1. 实用建议:

a. 工具函数支持:

// 类型转换映射
var typeMapping = map[string]string{
    "varchar":  "string",
    "char":     "string",
    "text":     "string",
    "int":      "int32",
    "bigint":   "int64",
    "datetime": "time.Time",
}

// 命名转换
func toUpperCamelCase(s string) string {
    // 实现下划线到大驼峰的转换
    // user_name -> UserName
}

func toLowerCamelCase(s string) string {
    // 实现下划线到小驼峰的转换
    // user_name -> userName
}

b. 支持自定义配置:

type Config struct {
    PackageName     string
    OutputPath      string
    TemplateFile    string
    TypeMapping     map[string]string
    TagStyle       string
}

c. 增加更多模板:

  • CRUD操作模板
  • Service层模板
  • HTTP Handler模板
  • 测试代码模板

主要优点:

  1. 原生支持,无需额外依赖
  2. 性能好
  3. 使用简单
  4. 容易扩展

使用场景:

  1. 生成数据模型代码
  2. 生成CRUD操作代码
  3. 生成API接口代码
  4. 生成测试代码

这样的代码生成器可以大大提高开发效率,特别是在处理大量重复性代码时。

用户:

我看到go语言里面的有一个mock-gen,这个可以对一些接口生成mock的代码,它会自动根据接口的名字和数据类型生成匹配的代码,这个是怎么做到的?

ChatGPT:

mockgen 是 Go 官方提供的一个代码生成工具,主要用于自动生成接口的 mock 实现。它的核心原理是通过解析 Go 源代码的 AST(抽象语法树),然后根据接口定义生成相应的 mock 代码。

以下是基本原理和简单示例:

  1. 首先看一个基础示例:
// db.go
package db

type DB interface {
    Get(id string) (string, error)
    Save(id string, value string) error
}

使用 mockgen 生成 mock:

mockgen -source=db.go -destination=mock_db.go -package=db

生成的 mock 代码大致如下:

// mock_db.go
package db

import (
    gomock "github.com/golang/mock/gomock"
)

// MockDB is a mock of DB interface
type MockDB struct {
    ctrl     *gomock.Controller
    recorder *MockDBMockRecorder
}

// MockDBMockRecorder is the mock recorder for MockDB
type MockDBMockRecorder struct {
    mock *MockDB
}

func NewMockDB(ctrl *gomock.Controller) *MockDB {
    mock := &MockDB{ctrl: ctrl}
    mock.recorder = &MockDBMockRecorder{mock}
    return mock
}

// Get mocks base method
func (m *MockDB) Get(id string) (string, error) {
    ret := m.ctrl.Call(m, "Get", id)
    ret0, _ := ret[0].(string)
    ret1, _ := ret[1].(error)
    return ret0, ret1
}

// Get indicates an expected call of Get
func (mr *MockDBMockRecorder) Get(id interface{}) *gomock.Call {
    return mr.mock.ctrl.RecordCallWithMethodName(mr.mock, "Get", id)
}
  1. 如果要实现类似的功能,核心步骤如下:
package main

import (
    "fmt"
    "go/ast"
    "go/parser"
    "go/token"
    "log"
    "os"
    "text/template"
)

// 接口方法信息
type MethodInfo struct {
    Name       string
    Params     []ParamInfo
    Results    []ParamInfo
}

type ParamInfo struct {
    Name string
    Type string
}

// 解析源代码
func parseInterface(filename string) ([]MethodInfo, error) {
    fset := token.NewFileSet()
    
    // 解析源文件
    node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
    if err != nil {
        return nil, err
    }

    var methods []MethodInfo

    // 遍历AST
    ast.Inspect(node, func(n ast.Node) bool {
        // 查找接口定义
        typeSpec, ok := n.(*ast.TypeSpec)
        if !ok {
            return true
        }

        interfaceType, ok := typeSpec.Type.(*ast.InterfaceType)
        if !ok {
            return true
        }

        // 遍历接口方法
        for _, method := range interfaceType.Methods.List {
            methodType := method.Type.(*ast.FuncType)
            
            mi := MethodInfo{
                Name:    method.Names[0].Name,
                Params:  parseFieldList(methodType.Params),
                Results: parseFieldList(methodType.Results),
            }
            
            methods = append(methods, mi)
        }

        return true
    })

    return methods, nil
}

// 解析参数或返回值列表
func parseFieldList(fieldList *ast.FieldList) []ParamInfo {
    var params []ParamInfo
    if fieldList == nil {
        return params
    }

    for _, field := range fieldList.List {
        typeName := getTypeString(field.Type)
        
        if len(field.Names) == 0 {
            params = append(params, ParamInfo{"", typeName})
        } else {
            for _, name := range field.Names {
                params = append(params, ParamInfo{
                    Name: name.Name,
                    Type: typeName,
                })
            }
        }
    }
    
    return params
}

// 获取类型字符串
func getTypeString(expr ast.Expr) string {
    switch t := expr.(type) {
    case *ast.Ident:
        return t.Name
    case *ast.StarExpr:
        return "*" + getTypeString(t.X)
    case *ast.SelectorExpr:
        return getTypeString(t.X) + "." + t.Sel.Name
    // 可以添加更多类型处理...
    default:
        return fmt.Sprintf("%T", expr)
    }
}

// mock代码模板
const mockTmpl = `
package {{.Package}}

import (
    "github.com/golang/mock/gomock"
)

type Mock{{.Interface}} struct {
    ctrl *gomock.Controller
}

func NewMock{{.Interface}}(ctrl *gomock.Controller) *Mock{{.Interface}} {
    return &Mock{{.Interface}}{ctrl: ctrl}
}

{{range .Methods}}
func (m *Mock{{$.Interface}}) {{.Name}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{.Name}} {{.Type}}{{end}}) ({{range $i, $r := .Results}}{{if $i}}, {{end}}{{.Type}}{{end}}) {
    ret := m.ctrl.Call(m, "{{.Name}}"{{range .Params}}, {{.Name}}{{end}})
    {{range $i, $r := .Results}}
    ret{{$i}} := ret[{{$i}}].({{.Type}})
    {{end}}
    return {{range $i, $r := .Results}}{{if $i}}, {{end}}ret{{$i}}{{end}}
}
{{end}}
`

func generateMock(methods []MethodInfo, packageName, interfaceName, outputFile string) error {
    tmpl, err := template.New("mock").Parse(mockTmpl)
    if err != nil {
        return err
    }

    f, err := os.Create(outputFile)
    if err != nil {
        return err
    }
    defer f.Close()

    data := struct {
        Package    string
        Interface  string
        Methods    []MethodInfo
    }{
        Package:    packageName,
        Interface:  interfaceName,
        Methods:    methods,
    }

    return tmpl.Execute(f, data)
}

func main() {
    methods, err := parseInterface("db.go")
    if err != nil {
        log.Fatal(err)
    }

    err = generateMock(methods, "db", "DB", "mock_db.go")
    if err != nil {
        log.Fatal(err)
    }
}
  1. 使用生成的mock:
func TestUsingMock(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockDB := NewMockDB(ctrl)
    
    // 设置期望
    mockDB.EXPECT().Get("123").Return("value", nil)
    
    // 使用mock
    value, err := mockDB.Get("123")
    if err != nil {
        t.Error(err)
    }
    if value != "value" {
        t.Errorf("expected 'value', got '%s'", value)
    }
}

核心实现原理:

  1. 代码解析
  • 使用 go/parser 解析源代码
  • 使用 go/ast 遍历语法树
  • 提取接口定义和方法信息
  1. 代码生成
  • 使用模板生成 mock 代码
  • 为每个方法生成对应的 mock 实现
  • 处理参数和返回值
  1. 特殊处理
  • 处理不同类型的参数
  • 处理导入包
  • 处理泛型(如果需要)

mockgen 的优势:

  1. 自动化:无需手动编写 mock 代码
  2. 类型安全:生成的代码保持类型正确
  3. 易于使用:与 testing 包良好集成
  4. 功能完善:支持期望值设置、调用次数验证等

这只是一个简化版的实现,实际的 mockgen 工具还包含更多功能:

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

推荐阅读更多精彩内容