【Android】静态代码检查-2.本地Hooks检查

一.Hooks检查大致流程

  1. 编写CheckStyle文件: 检查JavaCode的规范文件
  2. 编写Gradle:执行checkstyle task方法并调用checkstyle.xml对修改过的java文件进行检查
  3. 编写pre-commit脚本文件:在app目录下执行gradlew命令中的checkstyle task
  4. 编写commit-msg脚本文件:对代码提交所输入的日志进行规范检查
  5. 编写hooks脚本文件:自动在.git下生成pre-commit&commit-msg

二.Hooks检查代码

1. CheckStyle.xml文件:

如下所示一个比较完整的demo,检查的规范比较多,可以根据公司or项目组情况自定义规范。
注意几点:
a.配置文件中不能有重复的规则,否则创建或者导入时可能会导致失败。
b.配置文件中每一个<module> </module> 代表一个检测的功能点。
c.配置文件中的< property > </property > 可以配置一些参数,其中name="severity"参数表示该代码规范报错的严重程度,如果严重程度设置为error,则提交会不通过。

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE module PUBLIC
    "-//Puppy Crawl//DTD Check Configuration 1.3//EN"
    "http://www.puppycrawl.com/dtds/configuration_1_3.dtd">

<module name="Checker">
    <property name="charset" value="UTF-8" />
    <property name="severity" value="warning" />

    <!-- 检查文件是否以一个空行结束 -->
    <module name="NewlineAtEndOfFile" />

    <!-- 文件长度不超过1500行 -->
    <module name="FileLength">
        <property name="max" value="1500" />
    </module>

    <!-- 每个java文件一个语法树 -->
    <module name="TreeWalker">
        <!-- import检查-->
        <!-- 避免使用* -->
        <module name="AvoidStarImport">
            <property name="excludes" value="java.io,java.net,java.lang.Math" />
            <!-- 实例;import java.util.*;.-->
            <property name="allowClassImports" value="false" />
            <!-- 实例 ;import static org.junit.Assert.*;-->
            <property name="allowStaticMemberImports" value="true" />
        </module>
        <!-- 检查是否从非法的包中导入了类 -->
        <module name="IllegalImport" />
        <!-- 检查是否导入了多余的包 -->
        <module name="RedundantImport" />
        <!-- 没用的import检查,比如:1.没有被用到2.重复的3.import java.lang的4.import 与该类在同一个package的 -->
        <module name="UnusedImports" />


        <!-- 注释检查 -->
        <!-- 检查方法和构造函数的javadoc -->
        <module name="JavadocType">
            <property name="allowUnknownTags" value="true" />
            <message key="javadoc.missing" value="Class Comments: Lack of Javadoc annotations." />
        </module>
        <module name="JavadocMethod">
            <property name="tokens" value="METHOD_DEF" />
            <!--允许get set 方法没有注释-->
            <property name="allowMissingPropertyJavadoc" value="true" />
            <message key="javadoc.missing" value="Method Comments: Lack of Javadoc annotations." />
        </module>

        <!-- 命名检查 -->
        <!-- 局部的final变量,包括catch中的参数的检查 -->
        <module name="LocalFinalVariableName" />
        <!-- 局部的非final型的变量,包括catch中的参数的检查 -->
        <module name="LocalVariableName" />
        <!-- 包名的检查(只允许小写字母),默认^[a-z]+(\.[a-zA-Z_][a-zA-Z_0-9_]*)*$ -->
        <module name="PackageName">
            <!--<property name="severity" value="error" />-->
            <!--<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$" />-->
            <!--<message key="name.invalidPattern"-->
            <!--value="Class comment package names are allowed only in lowercase letters." />-->
        </module>
        <!-- 仅仅是static型的变量(不包括static final型)的检查 -->
        <module name="StaticVariableName" />
        <!-- Class或Interface名检查,默认^[A-Z][a-zA-Z0-9]*$-->
        <module name="TypeName">
            <property name="severity" value="warning" />
            <message key="name.invalidPattern"
                value="Class comment package names are allowed only in lowercase letters." />
        </module>
        <!-- 非static型变量的检查 -->
        <module name="MemberName" />
        <!-- 方法名的检查 -->
        <module name="MethodName" />
        <!-- 方法的参数名 -->
        <module name="ParameterName " />
        <!-- 常量名的检查(只允许大写),默认^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$ -->
        <module name="ConstantName" />

        <!-- 定义检查 -->
        <!-- 检查数组类型定义的样式 -->
        <module name="ArrayTypeStyle" />
        <!-- 检查long型定义是否有大写的“L” -->
        <module name="UpperEll" />

        <!-- 长度检查 -->
        <!-- 每行不超过120个字符 -->
        <module name="LineLength">
            <property name="max" value="120" />
        </module>
        <!-- 方法不超过50行 -->
        <module name="MethodLength">
            <property name="tokens" value="METHOD_DEF" />
            <property name="max" value="50" />
        </module>
        <!-- 方法的参数个数不超过5个。 并且不对构造方法进行检查-->
        <module name="ParameterNumber">
            <property name="max" value="5" />
            <property name="ignoreOverriddenMethods" value="true" />
            <property name="tokens" value="METHOD_DEF" />
            <property name="severity" value="error"/>
        </module>

        <!-- 空格检查-->
        <!-- 方法名后跟左圆括号"(" -->
        <module name="MethodParamPad" />
        <!-- 在类型转换时,不允许左圆括号右边有空格,也不允许与右圆括号左边有空格 -->
        <module name="TypecastParenPad" />
        <!-- 检查在某个特定关键字之后应保留空格 -->
        <module name="NoWhitespaceAfter" />
        <!-- 检查在某个特定关键字之前应保留空格 -->
        <module name="NoWhitespaceBefore" />
        <!-- 操作符换行策略检查 -->
        <module name="OperatorWrap" />
        <!-- 圆括号空白 -->
        <module name="ParenPad" />
        <!-- 检查分隔符是否在空白之后 -->
        <module name="WhitespaceAfter" />
        <!-- 检查分隔符周围是否有空白 -->
        <module name="WhitespaceAround" />

        <!-- 修饰符检查 -->
        <!-- 检查修饰符的顺序是否遵照java语言规范,默认public、protected、private、abstract、static、final、transient、volatile、synchronized、native、strictfp -->
        <module name="ModifierOrder" />
        <!-- 检查接口和annotation中是否有多余修饰符,如接口方法不必使用public -->
        <module name="RedundantModifier" />

        <!-- 代码块检查 -->
        <!-- 检查是否有嵌套代码块 -->
        <module name="AvoidNestedBlocks" />
        <!-- 检查是否有空代码块 -->
        <module name="EmptyBlock" />
        <!-- 检查左大括号位置 -->
        <module name="LeftCurly" />
        <!-- 检查代码块是否缺失{} -->
        <module name="NeedBraces" />
        <!-- 检查右大括号位置 -->
        <module name="RightCurly" />

        <!-- 代码检查 -->
        <!-- 检查空的代码段 -->
        <module name="EmptyStatement" />
        <!-- 检查在重写了equals方法后是否重写了hashCode方法 -->
        <module name="EqualsHashCode" />
        <!-- 检查局部变量或参数是否隐藏了类中的变量 -->
        <module name="HiddenField">
            <property name="tokens" value="VARIABLE_DEF" />
        </module>
        <!-- 检查是否使用工厂方法实例化 -->
        <module name="IllegalInstantiation" />
        <!-- 检查子表达式中是否有赋值操作 -->
        <module name="InnerAssignment" />
        <!-- 检查是否有"魔术"数字 -->
        <module name="MagicNumber">
            <property name="ignoreNumbers" value="0, 1" />
            <property name="ignoreAnnotation" value="true" />
        </module>
        <!-- 检查switch语句是否有default -->
        <module name="MissingSwitchDefault" />
        <!-- 检查是否有过度复杂的布尔表达式 -->
        <module name="SimplifyBooleanExpression" />
        <!-- 检查是否有过于复杂的布尔返回代码段 -->
        <module name="SimplifyBooleanReturn" />

        <!-- 类设计检查 -->
        <!-- 检查类是否为扩展设计l -->
        <!-- 检查只有private构造函数的类是否声明为final -->
        <module name="FinalClass" />
        <!-- 检查工具类是否有putblic的构造器 -->
        <module name="HideUtilityClassConstructor" />
        <!-- 检查接口是否仅定义类型 -->
        <module name="InterfaceIsType" />
        <!-- 检查类成员的可见度 检查类成员的可见性。只有static final 成员是public的
        除非在本检查的protectedAllowed和packagedAllowed属性中进行了设置-->
        <module name="VisibilityModifier">
            <property name="packageAllowed" value="true" />
            <property name="protectedAllowed" value="true" />
        </module>

        <!-- 语法 -->
        <!-- String的比较不能用!= 和 == -->
        <module name="StringLiteralEquality" />
        <!-- 限制for循环最多嵌套2层 -->
        <module name="NestedForDepth">
            <property name="max" value="2" />
        </module>
        <!-- if最多嵌套3层 -->
        <module name="NestedIfDepth">
            <property name="max" value="3" />
        </module>
        <!-- 检查未被注释的main方法,排除以Appllication结尾命名的类 -->
        <module name="UncommentedMain">
            <property name="excludedClasses" value=".*Application$" />
        </module>
        <!-- 禁止使用System.out.println -->
        <module name="Regexp">
            <property name="format" value="System\.out\.println" />
            <property name="illegalPattern" value="true" />
        </module>
        <!-- return个数 3个-->
        <module name="ReturnCount">
            <property name="max" value="3" />
        </module>
        <!--try catch 异常处理数量 3-->
        <module name="NestedTryDepth ">
            <property name="max" value="3" />
        </module>
        <!-- clone方法必须调用了super.clone() -->
        <module name="SuperClone" />
        <!-- finalize 必须调用了super.finalize() -->
        <module name="SuperFinalize" />

    </module>
</module>

2. Gradle文件:

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.4.1'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }

    apply plugin: 'checkstyle'

    checkstyle {
        toolVersion '6.13'
        ignoreFailures false
        showViolations true
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

// 脚本pre-commit 调用 checkstyle task 检查代码(修改提交的代码)
task checkstyle(type: Checkstyle) {
    source 'app/src/main/java'
    exclude '**/gen/**'
    exclude '**/R.java'
    exclude '**/BuildConfig.java'

    if (project.hasProperty('checkCommit') && project.property("checkCommit")) {
        List<String> fileS = filterCommitter(getChangeFiles())
        def includeList = new ArrayList<String>()
        for (String split : fileS) {
            String[] splits = split.split("/")
            String fileName = splits[splits.length - 1]
            includeList.add("**/" + fileName)
        }
        if (includeList.size() == 0) {
            exclude '**/*.java'
        } else {
            println("includeList==" + includeList)
            include includeList
        }
    } else {
        include '**/*.java'
    }
    configFile rootProject.file('hook/checkstyle.xml')
    classpath = files()
}

//根据git status 获取 修改的文件
def getChangeFiles() {
    try {
        String changeInfo = 'git status -s'.execute(null, project.rootDir).text.trim()
        return changeInfo == null ? "" : changeInfo
    } catch (Exception e) {
        return ""
    }
}

//只对java代码
def filterCommitter(String info) {
    ArrayList<String> filterList = new ArrayList<String>()
    String[] lines = info.split("\\n")
    for (String line : lines) {
        if (line.contains(".java")) {
            String[] split = line.trim().split(" ")
            for (String str : split) {
                if (str.contains(".java")) {
                    filterList.add(str)
                }
            }
        }
    }
    return filterList
}

3. pre-commit脚本代码:

#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments.  The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-commit".

if git rev-parse --verify HEAD >/dev/null 2>&1
then
    against=HEAD
else
    # Initial commit: diff against an empty tree object
    against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi

#开始 代码检查拦截
TAG="pre-commit log----------------->"

#打印success日志函数
printS(){
   echo -e "\033[0;32m $* \033[0m"
}

#打印fail日志函数
printE(){
   echo -e "\033[0;31m $* \033[0m"
}

#获取脚本的根目录
SCRIPT_DIR=$(dirname "$0")
#echo "SCRIPT_DIR=$SCRIPT_DIR"
SCRIPT_ABS_PATH=`cd "$SCRIPT_DIR"; pwd`
#echo "SCRIPT_ABS_PATH=$SCRIPT_ABS_PATH"
#在app项目目录下执行 gradlew 命令执行 checkstyle task
$SCRIPT_ABS_PATH/../../gradlew  -P checkCommit="true" checkstyle
if [ $? -eq 0   ]; then
# checkstyle检查成功
    info=$TAG"checkstyle OK"
    printS $info
else
#checkstyle检查失败
    info=$TAG"checkstyle fail"
    printE $info
    exit 1
fi
#结束

# If you want to allow non-ASCII filenames set this variable to true.
allownonascii=$(git config --bool hooks.allownonascii)

# Redirect output to stderr.
exec 1>&2

# Cross platform projects tend to avoid non-ASCII filenames; prevent
# them from being added to the repository. We exploit the fact that the
# printable range starts at the space character and ends with tilde.
if [ "$allownonascii" != "true" ] &&
    # Note that the use of brackets around a tr range is ok here, (it's
    # even required, for portability to Solaris 10's /usr/bin/tr), since
    # the square bracket bytes happen to fall in the designated range.
    test $(git diff --cached --name-only --diff-filter=A -z $against |
      LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
then
    cat <<\EOF
Error: Attempt to add a non-ASCII file name.

This can cause problems if you want to work with people on other platforms.

To be portable it is advisable to rename the file.

If you know what you are doing you can disable this check using:

  git config hooks.allownonascii true
EOF
    exit 1
fi

# If there are whitespace errors, print the offending file names and fail.
exec git diff-index --check --cached $against --

4. commit-msg脚本代码:

#!/bin/sh
#
# An example hook script to check the commit log message.
# Called by "git commit" with one argument, the name of the file
# that has the commit message.  The hook should exit with non-zero
# status after issuing an appropriate message if it wants to stop the
# commit.  The hook is allowed to edit the commit message file.
#
# To enable this hook, rename this file to "commit-msg".

# Uncomment the below to add a Signed-off-by line to the message.
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
# hook is more suited to it.
#
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"

# This example catches duplicate Signed-off-by lines.

#开始 日志拦截
TAG="commit-msg log----------------->"

#打印success日志函数
printS(){
    info=$TAG$*
    echo -e "\033[0;32m $info \033[0m"
}

#打印fail日志函数
printE(){
   info=$TAG$*
   echo -e "\033[0;31m $info \033[0m"
}

fileName=$1
#message 获取提交日志
message=$(<$fileName)
if [ -z $message ]
#message 为空
  then
  printE "submit message info is null"
  exit 1
fi


#例子,对下面的字符串模板解析
#[update|add|change|fix]xxx
printS $message
#日志最大长度
maxLen=30
#日志最小长度
minLen=4
sI=-1
eI=-1
#提交日志模板样式
logFormat="log format--->\"[update|add|change|fix]xxxxxxxx\""

#判断[]起始结束位置
sI=$(awk -v a="$message" -v b="[" 'BEGIN{print index(a,b)}')
eI=$(awk -v a="$message" -v b="]" 'BEGIN{print index(a,b)}')
#sI=`expr index "$message" [`
#eI=`expr index "$message" ]`
len=${#message}
eeI=`expr $eI-2`

#printS "sI=$sI,eI=$eI"

#判断[]开始
if [[ $sI -eq 1 && $eeI -ge $sI ]]
 then
    action=${message:$sI:$eeI}
    log=${message:$eI}
    logLen=${#log}
    #printS "action=$action,log=$log,logLen=$logLen"
    #提交action判断
    if [[ $action = "update" || $action = "add" || $action = "change" || $action = "fix" ]]
        then
        subLog=${log:0:1}
        #printS "log=$log,subLog=$subLog"
        #提交的本次记录内容
        if [ -z $log ]
            then
            printE "log is empty"
            exit 1
        elif [ $logLen -lt $minLen ]
            #提交的本次记录内容太少
            then
            printE "log is min $minLen"
            exit 1
        elif [ -z $subLog ]
            #提交的本次记录内容与】之间有空格,[update] XXXX
            then
            printE $logFormat
            exit 1
        elif [ $logLen -gt $maxLen ]
            #提交的本次记录内容太多
            then
            printE "log is max $maxLen"
            exit 1
        else
            printS "check log success"
        fi
    else
        printE $logFormat
        exit 1
    fi
 else
    printE $logFormat
    exit 1
fi
#结束

5. hooks脚本代码:

#!/bin/bash
#开始
# 复制 pre-commit commit-msg脚本到/.git/hooks/目录下
# r d
action='r'
#获取输入命令的第一个参数
cmd=$1
if [ $cmd ]
 then
  action=$1
fi
echo "action=$action"
# 获取hook下的commit-msg和pre-commit
precommitName="pre-commit"
commitName="commit-msg"
#获取当前目录
sourcePath=$(pwd)
sourcePrecommit=$sourcePath"/"$precommitName
sourceCommitmsg=$sourcePath"/"$commitName
echo "sourcePrecommit=$sourcePrecommit"
echo "sourceCommitmsg=$sourceCommitmsg"
cd ..
rootPath=$(pwd)
echo "rootPath=$rootPath"
gitDir=$rootPath"/.git"
echo "gitDir=$gitDir"
hooksDir=$gitDir"/hooks"
echo "hooksDir=$hooksDir"
pre_hooks=$hooksDir"/"$precommitName
commit_hooks=$hooksDir"/"$commitName

# 创建一个.git目录
if [ -d $gitDir ]
then
 echo "$gitDir exsit"
 if [ -d $hooksDir ]
  then
   echo "$hooksDir exsit"
  else
   echo "$hooksDir not exsit"
   # mkdir 创建一个新目录
   mkdir $hooksDir
   echo "$hooksDir mkdir ok"
 fi
else
 echo "$gitDir not exsit"
 git init
 echo "git init ok"
fi
#复制 pre_commit 文件
if [ -e $pre_hooks ]
then
 echo "$pre_hooks exsit"
 if [ $action = "d" ]
 then
    rm -r $pre_hooks
    echo "$pre_hooks remove ok"
 else
    cp $sourcePrecommit $hooksDir
    chmod 777 $pre_hooks
    echo "$pre_hooks replace ok"
 fi
else
 echo "$pre_hooks not exsit"
 cp $sourcePrecommit $hooksDir
 chmod 777 $pre_hooks
 echo "$pre_hooks copy ok"
fi

#复制 commit-msg 文件  -e 判断文件是否存在
if [ -e $commit_hooks ]
then
  echo "$commit_hooks exsit"
  if [ $action = "d" ]
  then
    # rm -r 递归删除
    rm -r $commit_hooks
    echo "$commit_hooks remove ok"
  else
    # cp复制
    cp $sourceCommitmsg $hooksDir
    chmod 777 $commit_hooks
    echo "$commit_hooks replace ok"
  fi
else
 echo "$commit_hooks not exsit"
 cp $sourceCommitmsg $hooksDir
 chmod 777 $commit_hooks
 echo "$commit_hooks copy ok"
fi
#结束

三.如何集成到项目以及演示(以下演示都是基于Android studio以及Mac系统)

1.Hook下5个配置文件在Project下的位置如图:

1 配置文件位置

2.运行hooks.sh,把pre-commit & commit-msg配置到 .git下。

第一步.找到Project下 .git中的hooks查看里面内容(如果没有初始化git,可以在命令行输入git init, 也可以不初始化,hooks.sh脚本中配置了初始化git)。
2.1 脚本执行前hooks下的内容

第二步.在Android studio中的Terminal运行命令cd hook,进入hook文件。
2.2 进入hook文件

第三步.输入chmod 777 hooks.sh,给hooks.sh脚本增加可执行权限。
2.3 给脚本添加可执行权限

第四步.输入./hooks.sh。这时候我们再打开 .git下的hooks会发现pre-commit & commit-msg已经copy到该目录下
2.4 执行hooks.sh脚本
2.5 脚本执行后hooks下的内容

第五步.输入cd ..,退出hook文件夹
2.6 退出hook文件夹

3.编写一份Test代码,然后输入 git add. + git commit -m test提交(提交失败)

package hulk.com;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void test(String s1, String s2, String s3, String s4, String s5, String s6) {

    }
}
3.1 提交失败1

4.修改error级别错误代码后,然后输入 git add. + git commit -m test提交(提交失败)

3.2 提交失败2

5.输入 git add. + git commit -m [update]test提交

3.3 提交成功

四.除了用shell脚本语言配置Hooks外,是否还能用其他脚本语言进行配置?

内置的脚本大多都是shell和PERL语言,但是我们可以使用任何脚本语言包括当前市场上最火热的Python语言,只要它们最后能编译到可执行文件当中即可。每次脚本中的#!/bin/sh定义了你的文件将被如何解释。比如,使用其他语言时你只需要将path改为你的解释器的路径。

五.Hooks检查的优势之处与不足之处在哪

相对与代码的自测,Hooks检查可谓是进了一大步,它是强制,一次配置终身检查,它是自动的,即我们每次提交代码时它都会自动拦截,它不但可以对你提交的内容进行检查,还可以对你提交的日志进行规范,使我们的每一次提交变得规范与严谨。但是它还是有不足之处,第一,它只能对当前提交的代码进行检查,我们无法追溯历史代码;第二,如果开发人员不对Hooks进行配置或者对配置好的Hooks进行修改,那么可以达到逃避检查的目的。针对以上两点,我们就需要引入Jenkins打包时对代码进行进一步扫描。


最后附上项目github地址:

hooks检查.git

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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