如何发布Android库到Maven中心仓库

前言

本文用于记录如何将自己的库上传到maven中心仓库,

  • 首先我们需要注册sonatype的jira账号,然后申请创建自己的repo,等待官方审核通过之后即可拥有自己的空间;
  • 我们使用gradle的maven-publish和signing插件来简化打包上传的操作,通过配置之后,即可通过gradle任务来上传到maven仓库;
  • 上传时可以选择上传到snapshot存储区或者staging存储区,这两个存储去上传之后即可立刻访问,snapshot区可公开访问,而staging只能供自己或有权限的人使用,需要验证用户名密码;
  • 如需要将staging区的版本公开给所有人使用,可通过sonatype网站上的release操作来公开;

创建sonatype账号及group

参考: OSSRH Guide - The Central Repository Documentation (sonatype.org)

  1. 创建Jira账号

  2. 创建issues: 创建问题 - Sonatype JIRA

创建issues并且经管理员同意后,才可以上传仓库,创建之后相当于拥有了一个group,之后可以往这个group上传其他的项目而无需再次建立issues;

image-20210525091621597

新建issues之后,系统会回复,根据回复的要求进行处理即可,通过后会有类似的回复:

com.github.hanlyjiang has been prepared, now user(s) hanlyjiang can:
Deploy snapshot artifacts into repository https://oss.sonatype.org/content/repositories/snapshots
Deploy release artifacts into the staging repository https://oss.sonatype.org/service/local/staging/deploy/maven2
Release staged artifacts into repository 'Releases'
please comment on this ticket when you promoted your first release, thanks

即表明我们拥有了自己的group;

生成签名key

所有上传到仓库中的文件必须进行签名,否则会无法发布,所以我们需要生成签名用的key,同时还需要将key推送到公共的key服务器,然sonatype服务器可以访问到,以进行验证;

生成key

过程中需要输入密码,==输入后请记住密码==

gpg --gen-key
gpg (GnuPG) 2.2.22; Copyright (C) 2020 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

gpg: 钥匙箱‘/Users/hanlyjiang/.gnupg/pubring.kbx’已创建
注意:使用 “gpg --full-generate-key” 以获得一个功能完整的密钥产生对话框。

GnuPG 需要构建用户标识以辨认您的密钥。

真实姓名: hanlyjiang
电子邮件地址: hanlyjiang@outlook.com
您选定了此用户标识:
    “hanlyjiang <hanlyjiang@outlook.com>”

更改姓名(N)、注释(C)、电子邮件地址(E)或确定(O)/退出(Q)? o
我们需要生成大量的随机字节。在质数生成期间做些其他操作(敲打键盘
、移动鼠标、读写硬盘之类的)将会是一个不错的主意;这会让随机数
发生器有更好的机会获得足够的熵。
我们需要生成大量的随机字节。在质数生成期间做些其他操作(敲打键盘
、移动鼠标、读写硬盘之类的)将会是一个不错的主意;这会让随机数
发生器有更好的机会获得足够的熵。
gpg: 密钥 E8A99FE282B70849 被标记为绝对信任
gpg: 吊销证书已被存储为‘/Users/hanlyjiang/.gnupg/openpgp-revocs.d/0B372361CC1A9AE2452D43FDE8A99FE282B70849.rev’
公钥和私钥已经生成并被签名。

pub   rsa3072 2021-05-24 [SC] [有效至:2023-05-24]
      0B372361CC1A9AE2452D43FDE8A99FE282B70849
uid                      hanlyjiang <hanlyjiang@outlook.com>
sub   rsa3072 2021-05-24 [E] [有效至:2023-05-24]

列出key

gpg --list-keys
gpg: 正在检查信任度数据库
gpg: 绝对信任密钥 8F5EC255E5A0D063 的公钥未找到
gpg: 绝对信任密钥 4150E419D483B9A6 的公钥未找到
gpg: marginals needed: 3  completes needed: 1  trust model: pgp
gpg: 深度:0  有效性:  3  已签名:  0  信任度:0-,0q,0n,0m,0f,3u
gpg: 下次信任度数据库检查将于 2023-05-24 进行
/Users/hanlyjiang/.gnupg/pubring.kbx
------------------------------------
pub   rsa3072 2021-05-24 [SC] [有效至:2023-05-24]
      0B372361CC1A9AE2452D43FDE8A99FE282B70849
uid           [ 绝对 ] hanlyjiang <hanlyjiang@outlook.com>
sub   rsa3072 2021-05-24 [E] [有效至:2023-05-24]

发送key到服务器

提示:

如果一个key服务器不通的话,可以换一个重新来一遍,只要上传成功一个即可。

key需要发送到服务器上,以便sonatype获取并校验签名,通过如下命令上传:

设置key的信息:

KEY_SERVER=hkp://pool.sks-keyservers.net
KEY_ID=0B372361CC1A9AE2452D43FDE8A99FE282B70849

上传:

$ gpg --keyserver $KEY_SERVER --send-keys $KEY_ID
gpg: 正在发送密钥 E8A99FE282B70849 到 hkp://pool.sks-keyservers.net

查看是否成功:

KEY_SERVER=hkp://pool.sks-keyservers.net
gpg --keyserver $KEY_SERVER --recv-keys $KEY_ID

可用的key-server:

hkp://keyserver.ubuntu.com
hkp://pool.sks-keyservers.net
hkp://keys.openpgp.org
hkp://keys.gnupg.net
hkp://keys.openpgp.org

导出key

导出公钥:

gpg -a -o ~/.gnupg/maven-pub.key --export $KEY_ID

导出私钥:(需要输入密码)

gpg -a -o ~/.gnupg/maven-prv.key --export-secret-keys $KEY_ID

导出gpgkey:

gpg --keyring secring.gpg --export-secret-keys > ~/.gnupg/secring.gpg

gradle配置

官方关于Gradle下上传的说明在这里 Gradle - The Central Repository Documentation (sonatype.org),这个文章中使用的是maven插件,另外还有一个maven-publish插件,我们使用maven-publish这个插件;

gradle.properties

修改gradle配置:

#  ~/.gradle/gradle.properties 写入如下内容:

ossrhUsername=hanlyjiang # jira的用户名
ossrhPassword=#jira的密码

# 公钥ID的后8位 0B372361CC1A9AE2452D43FDE8A99FE282B70849
signing.keyId=82B70849
signing.password=生成key时的密码
signing.secretKeyRingFile=/Users/hanlyjiang/.gnupg/secring.gpg

用户名和密码可以随意命名,只要自己在build.gradle对应上就可以

而 signing 的配置则需要保持名称一致。

build.gradle:

参考:

我们在需要上传的项目中配置,注意pom中的信息也需要补全,否则上传之后无法通过sonatype的检查,无法发布;

⚠️注意: 有的人的上传地址可能是 https://s01.oss.sonatype.org 的域名,如:

  • https://s01.oss.sonatype.org/content/repositories/snapshots
    
  • https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/
    
plugins {
    id 'com.android.library'
    id 'signing'
    id 'maven-publish'
}

def VERSION="1.0.1"
android {
    defaultConfig {
        minSdkVersion 22
        targetSdkVersion 30
        versionCode 1
        versionName VERSION
    }
}


// Because the components are created only during the afterEvaluate phase, you must
// configure your publications using the afterEvaluate() lifecycle method.
afterEvaluate {
    publishing {
        repositories {
            maven {
                name "local"
                // change to point to your repo, e.g. http://my.org/repo
                url = "$buildDir/repo"
            }
            maven {
                name "sonartype-Staging"
                // change to point to your repo, e.g. http://my.org/repo
                url = "https://oss.sonatype.org/service/local/staging/deploy/maven2"
              //  https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/
                credentials {
                    username = ossrhUsername
                    password = ossrhPassword
                }
            }
            // 定义snapshot仓库
            maven {
                name "sonatype-Snapshots"
                // change to point to your repo, e.g. http://my.org/repo
                url = "https://oss.sonatype.org/content/repositories/snapshots/"
                credentials {
                    username = ossrhUsername
                    password = ossrhPassword
                }
            }
        }
        publications {
            // Creates a Maven publication called "release".
            release(MavenPublication) {
                // Applies the component for the release build variant.
                from components.release

                // You can then customize attributes of the publication as shown below.
                groupId = 'com.github.hanlyjiang'
                artifactId = 'apf_library'
                version = VERSION
                pom {
                    name = 'HJ Android Plugin Framework'
                    description = 'A Android Plugin Framework'
                    url = 'https://github.com/hanlyjiang/apf-library'
                    licenses {
                        license {
                            name='The Apache Software License, Version 2.0'
                            url='http://www.apache.org/licenses/LICENSE-2.0.txt'
                        }
                    }
                    developers {
                        developer {
                            id = 'hanlyjiang'
                            name = 'hanly jiang'
                            email = 'hanlyjiang@outlook.com'
                        }
                    }
                    scm {
                        connection = 'https://github.com/hanlyjiang/apf-library'
                        developerConnection = 'https://github.com/hanlyjiang/apf-library.git'
                        url = 'https://github.com/hanlyjiang/apf-library'
                    }
                }
            }
            // Creates a Maven publication called “debug”.
            debug(MavenPublication) {
                // Applies the component for the debug build variant.
                from components.debug

                groupId = 'com.github.hanlyjiang'
                artifactId = 'apf_library-debug'
                version = String.format("%s-SNAPSHOT",VERSION)

                pom {
                    name = 'HJ Android Plugin Framework'
                    description = 'A Android Plugin Framework'
                    url = 'https://github.com/hanlyjiang/apf-library'
                    licenses {
                        license {
                            name='The Apache Software License, Version 2.0'
                            url='http://www.apache.org/licenses/LICENSE-2.0.txt'
                        }
                    }
                    developers {
                        developer {
                            id = 'hanlyjiang'
                            name = 'hanly jiang'
                            email = 'hanlyjiang@outlook.com'
                        }
                    }
                    scm {
                        connection = 'https://github.com/hanlyjiang/apf-library'
                        developerConnection = 'https://github.com/hanlyjiang/apf-library.git'
                        url = 'https://github.com/hanlyjiang/apf-library'
                    }
                }
            }
        }

        signing {
            sign publishing.publications.release , publishing.publications.debug
        }
    }
}

发布脚本配置中有以下需要注意:

  • groupId:需要配置自己申请的 groupId;
  • artifactId:需要修改为自己项目的 artifactId;
  • pom 中的文件描述需要修改为自己项目的描述;
  • repositories 部分配置了远程仓库对应的用户名和密码,发布地址需要根据是否是新项目进行修改,旧项目域名是 oss.sonatype.org,新项目域名是:s01.oss.sonatype.org
  • signing 签名部分需要配置对应的 gpg 密钥和账户信息

关于maven 仓库的注意点:

  1. snapshots仓库上传的库,其版本号需要以 -SNAPSHOT 结尾,否则可能出现400错误;

执行上传任务

执行gradle 对应的任务即可上传

$ module=apf-library; ./gradlew "$module":publishReleasePublicationToCenterRepository

具体生成的任务可以在AndroidStudio 的Gradle工具窗口中查看publishing分组的任务;或者通过如下命令查看

$ module=apf-library; ./gradlew "$module":tasks| grep -E "publish|generate"

generateMetadataFileForDebugPublication - Generates the Gradle metadata file for publication 'debug'.
generateMetadataFileForReleasePublication - Generates the Gradle metadata file for publication 'release'.
generatePomFileForDebugPublication - Generates the Maven POM file for publication 'debug'.
generatePomFileForReleasePublication - Generates the Maven POM file for publication 'release'.
publish - Publishes all publications produced by this project.
publishAllPublicationsToCenterRepository - Publishes all Maven publications produced by this project to the center repository.
publishAllPublicationsToLocalRepository - Publishes all Maven publications produced by this project to the local repository.
publishDebugPublicationToCenterRepository - Publishes Maven publication 'debug' to Maven repository 'center'.
publishDebugPublicationToLocalRepository - Publishes Maven publication 'debug' to Maven repository 'local'.
publishDebugPublicationToMavenLocal - Publishes Maven publication 'debug' to the local Maven repository.
publishReleasePublicationToCenterRepository - Publishes Maven publication 'release' to Maven repository 'center'.
publishReleasePublicationToLocalRepository - Publishes Maven publication 'release' to Maven repository 'local'.
publishReleasePublicationToMavenLocal - Publishes Maven publication 'release' to the local Maven repository.
publishToMavenLocal - Publishes all Maven publications produced by this project to the local Maven cache.

发布

  • close
  • release

参考:

首先需要解释下这里的发布指的什么意思:我们的仓库上传之后,实际上是存储与一个临时的独立与公有仓库的地方,这个只能我们自己访问,如果需要将仓库提供给其他人访问,就需要发布;发布的过程可以手动在web页面上操作,也可以通过命令行来进行;

发布需要取sonatype的网站上操作,以下为操作步骤:

  • 打开 Nexus Repository Manager (sonatype.org)

  • 然后登录,登录之后可以看到 Build Promotion 的菜单,然后打开Staging Repository,其中会显示已经上传的仓库:

    image-20210524172822831
  • 选中一个staging repo,然后点击 Close,并进行确认

    image-20210524172941960
  • 结果可以在Activity中查看

    image-20210524173140850

    上面的错误是找不到key,我们重新将key上传到ubuntu:

gpg --keyserver $KEY_SERVER --send-keys 0B372361CC1A9AE2452D43FDE8A99FE282B70849
gpg --keyserver $KEY_SERVER --recv-keys 0B372361CC1A9AE2452D43FDE8A99FE282B70849
然后再次close
image-20210524173522647
  • 然后再进行release

    image-20210524173756512
  • 完成后,会发一个邮件通知,然后会更新jira上创建项目的issues:

    image-20210524181450169

引入并使用

引入snapshot或staging版本

可通过如下路径确认自己的库是否上传成功(注意将后面的路径替换为自己的):

snapshot和staging的仓库中的版本可在推送后立即访问,不过只能自己访问,需要验证用户名和密码。

添加以下repo配置:

allprojects {
    repositories {
        maven {
            name = "Sonatype-Snapshots"
            setUrl("https://oss.sonatype.org/content/repositories/snapshots")
//            setUrl("https://s01.oss.sonatype.org/content/repositories/snapshots")
            credentials(PasswordCredentials::class.java) {
                username = property("ossrhUsername").toString()
                password = property("ossrhPassword").toString()
            }
        }
        maven {
            name = "Sonatype-Staging"
            setUrl("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
//            setUrl("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/")
            credentials(PasswordCredentials::class.java) {
                username = property("ossrhUsername").toString()
                password = property("ossrhPassword").toString()
            }
        }
        google()
        jcenter()
        mavenCentral()
    }
}

引入release版本

如果需要公开发布给自己或其他人使用,则需要release,release操作之后距离可以访问到有一定的时间周期,下面是一次release后收到的官方的邮件:

Central sync is activated for com.github.hanlyjiang. After you successfully release, your component will be published to Central https://repo1.maven.org/maven2/, typically within 10 minutes, though updates to https://search.maven.org can take up to two hours.

也就是说从maven中心仓库中查询需要10分钟左右,从网页搜索则需要2个小时,可以访问:https://search.maven.org 来搜索,可以通过访问 https://repo1.maven.org/maven2/ 来确认是否被索引了,如果被索引,则可以引入到项目之中;

在gradle中使用则只需要导入mavenCenter() 即可;

rootProject build.gradle:

allprojects {
    repositories {
        google()
        jcenter()
        mavenCentral()
    }
}

app build.gradle:

implementation("com.github.hanlyjiang:apf_library:1.0")

kotlin中使用

配置

import org.gradle.api.publish.maven.MavenPom

plugins {
    id("com.android.library")
    id("signing")
    `maven-publish`
//    kotlin("android")
//    kotlin("android.extensions")
}

android {
    // 省略android配置
}

fun getMyPom(): Action<in MavenPom> {
    return Action<MavenPom> {
        name.set("Android Common Utils Lib")
        description.set("Android Common Utils Library For HJ")
        url.set("https://github.com/hanlyjiang/lib_common_utils")
        properties.set(mapOf(
                "myProp" to "value",
                "prop.with.dots" to "anotherValue"
        ))
        licenses {
            license {
                name.set("The Apache License, Version 2.0")
                url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
            }
        }
        developers {
            developer {
                id.set("hanlyjiang")
                name.set("Hanly Jiang")
                email.set("hanlyjiang@outlook.com")
            }
        }
        scm {
            connection.set("scm:git:git://github.com/hanlyjiang/lib_common_utils.git")
            developerConnection.set("scm:git:ssh://github.com/hanlyjiang/lib_common_utils.git")
            url.set("https://github.com/hanlyjiang/lib_common_utils")
        }
    }
}

afterEvaluate {
    publishing {
        publications {
            create<MavenPublication>("release") {
                from(components.getByName("release"))
                groupId = "com.github.hanlyjiang"
                artifactId = "android_common_utils"
                version = android.defaultConfig.versionName
                pom(getMyPom())
            }
        }

        repositories {
            val ossrhCredentials = Action<PasswordCredentials> {
                username = properties["ossrhUsername"].toString()
                password = properties["ossrhPassword"].toString()
            }
            // sonar的仓库,地址根据项目的版本号来确定是snapshot还是正式仓库
            maven {
                name = "Sonartype"

                val releasesRepoUrl = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2")
                val snapshotsRepoUrl = uri("https://oss.sonatype.org/content/repositories/snapshots/")
                url = if (android.defaultConfig.versionName.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl
                credentials(ossrhCredentials)
                // snapshot的地址:
                // https://oss.sonatype.org/content/repositories/snapshots/com/github/hanlyjiang/android_common_utils/
            }
            // 项目本地的仓库
            maven {
                name = "ProjectLocal"

                val releasesRepoUrl = uri(layout.buildDirectory.dir("repos/releases"))
                val snapshotsRepoUrl = uri(layout.buildDirectory.dir("repos/snapshots"))
                url = if (android.defaultConfig.versionName.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl
            }
        }
    }
    // https://stackoverflow.com/questions/54654376/why-is-publishing-function-not-being-found-in-my-custom-gradle-kts-file-within

    signing {
        sign(publishing.publications.getByName("release"))
    }

}

问题:

A problem occurred configuring project ':lib_common_utils'.
> SoftwareComponentInternal with name 'java' not found.

maven plugin - Why is 'publishing' function not being found in my custom gradle.kts file within buildSrc directory? - Stack Overflow

Maven Publish Plugin (gradle.org)

上传javadoc和source

添加javadoc和jarsource的任务

tasks.register("javadoc", Javadoc::class.java) {
    group = "publishing"
    dependsOn("assemble")
    source = android.sourceSets["main"].java.getSourceFiles()
    classpath += project.files(android.bootClasspath + File.pathSeparator)
    if (JavaVersion.current().isJava9Compatible) {
        (options as StandardJavadocDocletOptions).addBooleanOption("html5", true)
    }
    android.libraryVariants.forEach { libraryVariant ->
        classpath += libraryVariant.javaCompileProvider.get().classpath
    }
    options.apply {
        encoding("UTF-8")
        charset("UTF-8")
        isFailOnError = false

        (this as StandardJavadocDocletOptions).apply {
            addStringOption("Xdoclint:none")
            links?.add("https://developer.android.google.cn/reference/")
            links?.add("http://docs.oracle.com/javase/8/docs/api/")
        }
    }
}

tasks.register("jarSource", Jar::class.java) {
    group = "publishing"
    from(android.sourceSets["main"].java.srcDirs)
    archiveClassifier.set("sources")
}

tasks.register("jarJavadoc", Jar::class.java) {
    group = "publishing"
    dependsOn("javadoc")
    val javadoc: Javadoc = tasks.getByName("javadoc") as Javadoc
    from(javadoc.destinationDir)
    archiveClassifier.set("javadoc")
}

publish中使用

afterEvaluate {
    publishing {
        publications {
            create<MavenPublication>("release") {
                from(components.getByName("release"))
                groupId = "com.github.hanlyjiang"
                artifactId = "android_common_utils"
                version = android.defaultConfig.versionName
                pom(getMyPom())
                // 添加javadoc
                artifact(tasks.getByName("jarJavadoc") as Jar)
                // 添加source
                 artifact(tasks.getByName("jarSource") as Jar)
            }
        }

        repositories {
            val ossrhCredentials = Action<PasswordCredentials> {
                username = properties["ossrhUsername"].toString()
                password = properties["ossrhPassword"].toString()
            }
            // sonar的仓库,地址根据项目的版本号来确定是snapshot还是正式仓库
            maven {
                name = "Sonartype"

                val releasesRepoUrl = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2")
                val snapshotsRepoUrl = uri("https://oss.sonatype.org/content/repositories/snapshots/")
                url = if (android.defaultConfig.versionName.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl
                credentials(ossrhCredentials)
                // snapshot的地址:
                // https://oss.sonatype.org/content/repositories/snapshots/com/github/hanlyjiang/android_common_utils/
            }
            // 项目本地的仓库
            maven {
                name = "ProjectLocal"

                val releasesRepoUrl = uri(layout.buildDirectory.dir("repos/releases"))
                val snapshotsRepoUrl = uri(layout.buildDirectory.dir("repos/snapshots"))
                url = if (android.defaultConfig.versionName.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl
            }
        }
    }
    // https://stackoverflow.com/questions/54654376/why-is-publishing-function-not-being-found-in-my-custom-gradle-kts-file-within


    signing {
        sign(publishing.publications.getByName("release"))
    }

}

❌ 错误记录

400 错误

maven publish Received status code 400 from server

可能的原因: maven 仓库分两个,一个是snapshot仓库,一个是release 仓库,如果将snapshot版本(版本号带SNAPSHOT)的包上传到Release仓库的地址,则会报错。

参考文章

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

推荐阅读更多精彩内容