造一個方形的輪子9--編譯打包

造一個方形輪子文章目錄:造一個方形的輪子

01艾少、解決遺留問題

上一篇的最后說要把BeansInitUtil類代碼優(yōu)化一下,先來弄一下這個吧伦籍,順便把加入AOP導(dǎo)致多遍歷了一遍目錄的問題解決一下爆安。

調(diào)整的有點多醒串,就不放代碼了,寫一下主要改動的文件及改動內(nèi)容吧:

com.jisuye.core包:

BeansMap.java 全方法靜態(tài)化調(diào)用靜態(tài)的個各map容器

BeanObject.java 添加了BeanClass記錄bean類型宠叼,在加載AOP切面時使用

ControllerObject.java 添加了beanKey字段先巴,將獲取Bean從初始化階段放到調(diào)用階段,這么做是為了去掉添加AOP功能后多的一遍目錄遍歷

ControllerObject.java 部分代碼:

    /**
     * 反射執(zhí)行controller方法
     * @param req
     * @return
     */
    public Object invoke(HttpServletRequest req){
        //...
        try {
            // 延時從容器中獲取對象
            if(object == null){
                this.setObject(BeansMap.get(beanKey).getObject());
            }
            Object o = this.getMethod().invoke(this.getObject(), os);
            return o;
        } catch (Exception e) {
            log.error("Controller method.invoke() is error!", e);
            throw new SquareException("Controller method.invoke() is error!", e);
        }
    }

com.jisuye.util包:

BeansInitUtil.java 調(diào)整順序把AOP放到IOC初始化之后DI初始化之前冒冬,減少了一次目錄遍歷

BeansInitUtil.java 放部分代碼:

    public static void init(Class clazz){
        String path = clazz.getResource("").getPath();
        log.info("===bean init path:{}", path);
        File root = new File(path);
        // 處理控制反轉(zhuǎn)(加載aop切面,controller)
        initFile(root);
        // 處理aop類
        initAop();
        // 處理依賴注入
        initDI();
    }
    private static void initAop(){
        List<BeanObject> list = new ArrayList<>();
        // 循環(huán)所有Bean處理Aop
        for(Map.Entry entry : BeansMap.entrySet()) {
            BeanObject beanObject = (BeanObject) entry.getValue();
            // 如果已經(jīng)處理過伸蚯,則跳過
            if (beanObject.getObject() != null) {
                break;
            }
            beanObject.setObject(getInstance(beanObject.getBeanClass(), beanObject.getSrcObj()));
        }
    }
    // ...
    private static void loadClass(File file){
                    // ...
                    if(annotation instanceof Service){
                        tmp_name = ((Service)annotation).value();
                    } else if(annotation instanceof Component) {
                        tmp_name = ((Component)annotation).value();
                    } else if(annotation instanceof Controller) {
                        initController(clzz, ((Controller)annotation).value());
                    } else if(annotation instanceof Aspect){
                        // 添加加載AOP切面
                        loadAop(clzz);
                    }
                    // ...
    }

02、編譯打包問題整理

在真正開始寫編譯打包功能之前简烤,我一直覺得這應(yīng)該是一個比較簡單的功能剂邮,實現(xiàn)應(yīng)該也沒什么難度,事實是我的臉現(xiàn)在依然很疼横侦。挥萌。。

起初我想使用maven自帶的打包插件枉侧,打一個jar包出來引瀑,看了一下要指定主方法所在類,看起來有點麻煩榨馁,而且打出來看包是不包含依賴的憨栽,不可以直接運行,當然有其它的插件能夠?qū)崿F(xiàn)把依賴jar包打包進去的功能翼虫,但還要是配置主類屑柔,于是我想自己定一個插件吧(這大概是打臉的開始)。

只實現(xiàn)自定義插件很容易蛙讥,但要實現(xiàn)我的功能锯蛀,第一個功能就是指定Main-Class 看了一下灭衷,編譯完的文件里并沒有MANIFEST.MF清單文件次慢,只有最終打出來的jar包里才有,于是第一個問題就是:

如何向jar包里的文件寫入內(nèi)容?

看了spring-boot-maven-plugin 的源碼迫像,發(fā)現(xiàn)他是在package完成后跟著執(zhí)行了自己的repackage 流程劈愚,將package打出來的jar包,復(fù)制了一份闻妓。于是我也按這個思路實現(xiàn)了一下菌羽,在復(fù)制的過程中判斷是不是MANIFEST.MF 如果是的話向文件尾追加Main-Class配置 ,Main-Class指定的main方法所在類是通過遍歷目錄文件找到帶有@SquareApplication 注解的類獲得的由缆。Main-Class的問題解決了注祖,再來看一下沒有打包依賴的問題。

如何將依賴的jar打包到一起均唉?

有了上邊的經(jīng)驗是晨,這次和想直接把依賴的jar找出來,在復(fù)制的過程中保存進去這樣應(yīng)該就可以了舔箭,事實是我做到了罩缴,保存到了jar包的lib目錄下,結(jié)果依賴還是找不到层扶,后來查了一下箫章,在jar內(nèi)部的文件,引用的時候都是通過***.jar!/a/b/c 這樣的方式標記的镜会,Java中自帶的ClassLoader只能接受一個“!”的這種路徑檬寂,也就是可以加載jar包里的類,但對jar包里的jar包稚叹,也就是路徑上要帶兩個“!” 這種就無能為力了焰薄,SpringBoot 是自己實現(xiàn)了一個ClassLoader,重寫加載路徑方法扒袖,搞定的塞茅,簡單實現(xiàn)的話我參考了一下maven-assembly-plugin 他是將依賴jar包里的內(nèi)容全部解壓到當前jar里,這樣就不會出來找不到依賴的類的問題(依然簡單粗暴)季率。

編譯打包的問題解決了野瘦,打出了可以執(zhí)行的jar包,然后就發(fā)現(xiàn)我的程序?qū)崿F(xiàn)有很大的問題飒泻,之前大量使用的File加載反射類鞭光,是沒有問題的,但是放到Jar包中就不行了泞遗,因為File不能直接使用***.jar!/a/b這種形式的路徑惰许。也就不能使用File進行目錄遍歷。

如何在Jar包中遍歷路徑史辙?

這個問題我能想到的辦法就是在程序中特殊處理了汹买,判斷一下當前是在jar包中還是在程序中佩伤,如果是在jar包中,則加載JarFile 獲取Jar包中的條目晦毙,遍歷是否需要加載就可以了生巡。

03、編譯打包插件

這里先看一下核心的代碼见妒,全部代碼文章最后會給出下載地址孤荣。

BuildMojo.java:

package com.jisuye;
// import ...
@Mojo(name = "repackage", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true,
        requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
        requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
public class BuildMojo extends AbstractMojo {
    @Parameter(defaultValue = "${project}", readonly = true, required = true)
    private MavenProject project;

    @Component
    private MavenProjectHelper projectHelper;

    @Parameter(defaultValue = "${project.build.directory}", required = true)
    private File outputDirectory;

    @Parameter(defaultValue = "${project.build.finalName}", readonly = true)
    private String finalName;

    public void execute() throws MojoExecutionException, MojoFailureException {
        getLog().info("square maven plugin exe...");
        getLog().info("outputDir:"+outputDirectory.getPath());
        Artifact source = project.getArtifact();
        try {
            // 取package打出的jar包
            File file = source.getFile();
            getLog().info("file name name ======"+file.getName());
            JarFile jarFile = new JarFile(file);
            // 創(chuàng)建臨時jar包
            File tmpFile = new File(outputDirectory.getPath()+"/temp.jar");
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile));
            List<String> fileList = new ArrayList<String>();
            Enumeration<?> jarEntries = jarFile.entries();
            // 循環(huán)復(fù)制文件
            while (jarEntries.hasMoreElements()) {
                JarEntry entry = (JarEntry) jarEntries.nextElement();
//                getLog().info("filename:"+entry.getName());
                fileList.add(entry.getName());
                InputStream entryInputStream = jarFile.getInputStream(entry);
                // 如果是清單文件,則追加主類
                if(entry.getName().endsWith("MANIFEST.MF")){
                    BufferedReader br = new BufferedReader(new InputStreamReader(entryInputStream));
                    String line;
                    StringBuilder sb = new StringBuilder();
                    while ((line = br.readLine()) != null) {
                        if(!line.equals("")) {
                            sb.append(line).append("\n");
                        }
                    }
                    String mainClass = findMainClass();
                    getLog().info("mainclass:"+mainClass);
                    if(mainClass.equals("")){
                        getLog().error("No found Main-Class!!  Plase use the @SquareApplication settings");
                    } else {
                        sb.append("Main-Class: ").append(mainClass).append("\n");
                    }
                    jarOutputStream.putNextEntry(new JarEntry(entry.getName()));
                    jarOutputStream.write(sb.toString().getBytes());
                } else {
                    jarOutputStream.putNextEntry(entry);
                    byte[] buffer = new byte[1024];
                    int bytesRead = 0;
                    while ((bytesRead = entryInputStream.read(buffer)) != -1) {
                        jarOutputStream.write(buffer, 0, bytesRead);
                    }
                }
            }
            addDependenceJar(project.getArtifacts(), jarOutputStream, fileList);
            jarOutputStream.close();
            updateJar(file, tmpFile);
        } catch (IOException e) {
            getLog().error("load jar file error!", e);
        }
    }

    /**
     * 更新jar文件须揣,將原文件保存為**.jar.old
     * @param oldFile 原文件
     * @param newFile 添加清單及依賴后的完整包
     * @throws IOException
     */
    private void updateJar(File oldFile, File newFile) throws IOException {
        String fileName = oldFile.getName();
        File backup = new File(outputDirectory.getPath()+"/"+fileName+".old");
        FileOutputStream fos = new FileOutputStream(backup);
        FileInputStream fis = new FileInputStream(oldFile);
        byte[] buffer = new byte[1024];
        int bytesRead = 0;
        while ((bytesRead = fis.read(buffer)) != -1) {
            fos.write(buffer, 0, bytesRead);
        }
        fis.close();
        fos.close();
        oldFile.delete();
        if(finalName != null && !finalName.equals("")){
            fileName = finalName+".jar";
        }
        File newJar = new File(outputDirectory.getPath()+"/"+fileName);
        fos = new FileOutputStream(newJar);
        fis = new FileInputStream(newFile);
        buffer = new byte[1024];
        while ((bytesRead = fis.read(buffer)) != -1) {
            fos.write(buffer, 0, bytesRead);
        }
        fis.close();
        fos.close();
        newFile.delete();
    }

    /**
     * 添加依賴的jar
     * @param artifacts 依賴Jar列表
     * @param jarOutputStream 當前repackage包輸出流
     * @param fileList 已有文件列表(防止重復(fù))
     * @throws IOException
     */
    private void addDependenceJar(Set<Artifact> artifacts, JarOutputStream jarOutputStream, List<String> fileList) throws IOException {
        // save dependence jar
        for (Artifact artifact : artifacts) {
            JarFile jarFile = new JarFile(artifact.getFile());
            Enumeration<?> jarEntries = jarFile.entries();
            while (jarEntries.hasMoreElements()) {
                JarEntry entry = (JarEntry) jarEntries.nextElement();
                if(fileList.contains(entry.getName())){
                    // already added skipping
//                    getLog().info(entry.getName()+" already added, skipping");
                    continue;
                }
                fileList.add(entry.getName());
                InputStream entryInputStream = jarFile.getInputStream(entry);
                jarOutputStream.putNextEntry(entry);
                byte[] buffer = new byte[1024];
                int bytesRead = 0;
                while ((bytesRead = entryInputStream.read(buffer)) != -1) {
                    jarOutputStream.write(buffer, 0, bytesRead);
                }
            }
        }
    }

    /**
     * 遍歷目錄查找主類
     * @return
     */
    private String findMainClass(){
        Build build = project.getBuild();
        getLog().info("build source dir:"+build.getSourceDirectory());
        File f = new File(build.getSourceDirectory());
        StringBuilder mainClass = new StringBuilder();
        getMainClass(f, mainClass);
        return mainClass.toString();
    }

    /**
     * 查找還@SquareApplication注解的類
     * @param file 文件
     * @param mainClass 要返回的主類
     */
    private void getMainClass(File file, StringBuilder mainClass){
        if(!mainClass.toString().equals("")){
            return;
        }
        File[] fs = file.listFiles();
        for (File f : fs) {
            if(f.isDirectory()){
                // 遞歸目錄
                getMainClass(f, mainClass);
            } else {
                // 處理class
                try {
                    if(!f.getName().endsWith(".java")){
                        return;
                    }
                    BufferedReader br = new BufferedReader(new FileReader(f));
                    String line, packageStr = "";
                    while((line = br.readLine()) != null){
                        line = line.trim();
                        if(line.startsWith("package ")){
                            packageStr = line.substring(8).replace(";", "");
                        }
                        if(line.equals("@SquareApplication")){
                            mainClass.append(packageStr+"."+f.getName().replace(".java", ""));
                        }
                    }
                } catch (Exception e) {
                    getLog().error("Find Main-Class error!", e);
                }
            }
        }
    }
}

代碼中有注釋盐股,基本就是按前邊說的思路實現(xiàn)了一下。這塊除了對maven插件不太熟悉耻卡,別的都還好遂庄,多看看前輩們寫的插件,習(xí)慣就好了劲赠。

04涛目、Square框架修改

如上文所說,打包這后凛澎,原來的代碼問題很大霹肝,改了一些地方。

ClassesPathUtil.java:

    //...
    public ClassesPathUtil(Class clzz){
        String basePath = clzz.getResource("").getPath();
        log.info("basePath+++++{}", basePath);
        //  ..../classes
        if(basePath.indexOf("classes")>0) {
            projectPath = basePath.substring(0, basePath.indexOf("classes") + 7);
        } else {
            projectPath = basePath.substring(0, basePath.indexOf("!")+1);
        }
        publicPath = setPublic(projectPath, "/public");
    }
    //...

這里要加上判斷是在jar包中的情況塑煎,要特殊處理沫换。

LoadApplicationYmlUtil.java:

    // ...
    public static Map<String, Object> load(String projectPath){
        Map<String, Object> retMap = new HashMap<>();
        try {
            InputStream is;
            if(projectPath != null &&  projectPath.indexOf("!") > 0){
                is = ClassLoader.getSystemResourceAsStream("application.yml");
            } else {
                projectPath += "/application.yml";
                log.info("load yml file path:{}", projectPath);
                is = new FileInputStream(projectPath);
            }
            Yaml yaml = new Yaml();
            Map<String, Object> map = (Map<String, Object>)yaml.load(is);
            if(map != null && map.size()>0){
                for(Map.Entry e : map.entrySet()) {
                    convert("", retMap, e);
                }
            }
        } catch (FileNotFoundException e) {
            log.error("load application.yml file error.", e);
        }
        return retMap;
    }
    // ...

這里也要判斷如果是在jar包里就不能使用File的方式獲取文件流,可以通過ClassLoader.getSystemResourceAsStream()方法獲得最铁。

BeanInitUtil.java:

    // ...
     public static void init(Class clazz){
        String path = clazz.getResource("").getPath();
        log.info("===bean init path:{}", path);
        if(path.indexOf("!")<0) {
            File root = new File(path);
            // 處理控制反轉(zhuǎn)(加載aop切面,controller)
            initFile(root);
        } else {
            // 處理jar包內(nèi)的反射邏輯
            initJar(path);
        }
        // 處理aop類
        initAop();
        // 處理依賴注入
        initDI();
    }
    
    private static void initJar(String jarPath){
        try {
            String packageStr = jarPath.substring(jarPath.indexOf("!")+2).replaceAll("/", ".");
            log.info("packageStr :{}", packageStr);
            jarPath = jarPath.substring(0, jarPath.indexOf("!")).replace("file:/", "");
            log.info("jar file path:{}", jarPath);
            JarFile jarFile = new JarFile(new File(jarPath));
            // 獲取jar文件條目
            Enumeration<JarEntry> enumFiles = jarFile.entries();
            JarEntry entry;
            while(enumFiles.hasMoreElements()){
                entry = enumFiles.nextElement();
                String className = entry.getName().replaceAll("/", ".");
                // 只處理自己包下的class文件
                if(className.startsWith(packageStr) && className.indexOf(".class")>=0){
                    className = className.substring(0,className.length()-6).replace("/", ".");
                    log.info("class:{}", className);
                    loadClass(className);
                }
            }
        } catch (IOException e) {
            log.error("load jar file error!", e);
        }
    }

添加了initJar方法讯赏,從jar包中遍歷初始化Bean。

其它的一些聯(lián)動修改冷尉,可以在源碼中查看漱挎。

05、測試項目

新創(chuàng)建一個square-demo項目雀哨,pom.xml文件中引入我的們框架及打包工具(需要先在框架及打包工具目錄下執(zhí)行mvn clean install)

    <dependencies>
        <dependency>
            <groupId>com.jisuye</groupId>
            <artifactId>square</artifactId>
            <version>0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>com.jisuye</groupId>
                <artifactId>square-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

創(chuàng)建一個測試的HelloController.java:

package com.jisuye.controller;
// import ...
@Controller("/")
public class HelloController {
    @GetMapping("/hello")
    public String hello(@RequestParam("name") String name){
        return "hello "+name;
    }
}

啟動程序T.java:

package com.jisuye;
// import ...
@SquareApplication
public class T {
    public static void main(String args[]){
        run(T.class, args);
    }
}

添加application.yml:

server:
  port: 8765
  servlet:
    context-path: /square-demo

好的磕谅,現(xiàn)在我們試一下開發(fā)中啟動程序,支持T.main()方法雾棺,查看控制臺輸出:

18:58:04.793 [main] INFO com.jisuye.core.SquareApplication - 
 __________  ____
|   ____   |/ ___|  __ _ _   _  __ _ _ __ ___
|  |    |  |\___ \ / _` | | | |/ _` | '__/ _ \
|  |____|  | ___) | (_| | |_| | (_| | | |  __/
|__________||____/ \__, |\__,_|\__,_|_|  \___|
======================|_|====================
 :: Square ::        (v0.1)

18:58:04.794 [main] INFO com.jisuye.util.BeansInitUtil - ===bean init path:/C:/Users/admin/idea/square-demo/target/classes/com/jisuye/
18:58:04.795 [main] INFO com.jisuye.util.BeansInitUtil - load bean class:com.jisuye.controller.HelloController
18:58:04.805 [main] INFO com.jisuye.util.BeansInitUtil - classPath:, methods.length:10
18:58:04.813 [main] INFO com.jisuye.util.BeansInitUtil - add controller key:get:/hello
18:58:04.813 [main] INFO com.jisuye.util.BeansInitUtil - load bean class:com.jisuye.T
18:58:04.813 [main] INFO com.jisuye.core.SquareApplication - beans size is:3
....
18:58:06.911 [main] INFO com.jisuye.core.SquareApplication - Tomcat started on port(s): 8765 with context path '/square-demo'
18:58:06.911 [main] INFO com.jisuye.core.SquareApplication - Started Application in 2233 ms.

程序啟動成功膊夹,在瀏覽器訪問:http://localhost:8765/square-demo/hello?name=ixx

返回:hello ixx

到目前為止都沒有問題,然后我們來試一下打包捌浩,在square-demo項目目錄下執(zhí)行mvn clean package放刨,查看輸出:

....
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ square-demo ---
[INFO] Building jar: C:\Users\admin\idea\square-demo\target\square-demo-1.0-SNAPSHOT.jar
[INFO]
[INFO] --- square-maven-plugin:0.1-SNAPSHOT:repackage (default) @ square-demo ---
[INFO] square maven plugin exe...
[INFO] outputDir:C:\Users\admin\idea\square-demo\target
[INFO] file name name ======square-demo-1.0-SNAPSHOT.jar
[INFO] build source dir:C:\Users\admin\idea\square-demo\src\main\java
[INFO] mainclass:com.jisuye.T
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.737 s
[INFO] Finished at: 2019-08-14T19:00:23+08:00
[INFO] ------------------------------------------------------------------------

build success! 我們來看一下target目錄:

C:\Users\admin\idea\square-demo>ls target
classes  maven-archiver  maven-status  square-demo-1.0-SNAPSHOT.jar  square-demo-1.0-SNAPSHOT.jar.old

有一個square-demo-1.0-SNAPSHOT.jar 和一個square-demo-1.0-SNAPSHOT.jar.old,跟我們插件實現(xiàn)效果一樣尸饺,.old是package打出來的而.jar是我們repackage過的进统,我們來啟動看一下拓诸,執(zhí)行java -jar target/square-demo-1.0-SNAPSHOT.jar

.....
19:04:17.672 [main] INFO com.jisuye.core.SquareApplication - Tomcat started on port(s): 8765 with context path '/square-demo'
19:04:17.673 [main] INFO com.jisuye.core.SquareApplication - Started Application in 944 ms.

可以看到也啟動成功了,現(xiàn)在訪問一下

在瀏覽器訪問:http://localhost:8765/square-demo/hello?name=ixx

返回:hello ixx

06麻昼、遺留問題

編譯打包基本完成了,但還有很多問題:

1馋辈、沒有實現(xiàn)靜態(tài)文件目錄抚芦,public 的復(fù)制

2、沒有處Square理框架的依賴關(guān)系

3迈螟、現(xiàn)有方式加載Bean有可能加載多余的類

問題留給下一篇....

這一篇寫出來感覺沒多少東西但在做的過程中確實是出現(xiàn)太多跟預(yù)想的不一樣的結(jié)果叉抡,往往是為了解決一個問題,中間要走錯很多路答毫,找時間得好好看看Maven的實現(xiàn)原理褥民。

本篇代碼地址: https://github.com/iuv/square/tree/square9
打包插件地址: https://github.com/iuv/square-maven-plugin
演示項目地址: https://github.com/iuv/square-demo

本文作者: ixx
本文鏈接: http://jianpage.com/2019/08/15/square9
版權(quán)聲明: 本作品采用 知識共享署名-非商業(yè)性使用-相同方式共享 4.0 國際許可協(xié)議 進行許可。轉(zhuǎn)載請注明出處洗搂!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末消返,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子耘拇,更是在濱河造成了極大的恐慌撵颊,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惫叛,死亡現(xiàn)場離奇詭異倡勇,居然都是意外死亡,警方通過查閱死者的電腦和手機嘉涌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進店門妻熊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人仑最,你說我怎么就攤上這事扔役。” “怎么了警医?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵厅目,是天一觀的道長。 經(jīng)常有香客問我法严,道長损敷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任深啤,我火速辦了婚禮拗馒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘溯街。我一直安慰自己诱桂,他們只是感情好洋丐,可當我...
    茶點故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著挥等,像睡著了一般友绝。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上肝劲,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天迁客,我揣著相機與錄音,去河邊找鬼辞槐。 笑死掷漱,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的榄檬。 我是一名探鬼主播卜范,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼鹿榜!你這毒婦竟也來了海雪?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤舱殿,失蹤者是張志新(化名)和其女友劉穎喳魏,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體怀薛,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡刺彩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了枝恋。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片创倔。...
    茶點故事閱讀 40,001評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖焚碌,靈堂內(nèi)的尸體忽然破棺而出畦攘,到底是詐尸還是另有隱情,我是刑警寧澤十电,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布知押,位于F島的核電站,受9級特大地震影響鹃骂,放射性物質(zhì)發(fā)生泄漏台盯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一畏线、第九天 我趴在偏房一處隱蔽的房頂上張望静盅。 院中可真熱鬧,春花似錦寝殴、人聲如沸蒿叠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽市咽。三九已至痊银,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間施绎,已是汗流浹背溯革。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留粘姜,地道東北人。 一個月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓熔酷,卻偏偏與公主長得像孤紧,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子拒秘,可洞房花燭夜當晚...
    茶點故事閱讀 44,955評論 2 355

推薦閱讀更多精彩內(nèi)容