[02][01][02] 用300行代码手写提炼Spring的核心原理

[TOC]

实现思路

先来介绍一下 Mini 版本的 Spring 基本实现思路,如下图所示:


image

自定义配置

配置 application.properties 文件

为了解析方便,我们用 application.properties 来代替 application.xml 文件,具体配置内容如下:

scanPackage=com.gupaoedu.demo

配置 web.xml 文件

大家都知道,所有依赖于 web 容器的项目,都是从读取 web.xml 文件开始的.我们先配置好 web.xml
中的内容

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w 3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/j 2 ee"
    xmlns:javaee="http://java.sun.com/xml/ns/javaee"
    xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j 2 ee http://java.sun.com/xml/ns/j 2 ee/web-app_2_4.xsd" version="2.4">
    <display-name>Gupao Web Application</display-name>
    <servlet>
        <servlet-name>gpmvc</servlet-name>
        <servlet-class>com.gupaoedu.mvcframework.v 1.servlet.GPDispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>application.properties</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>gpmvc</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

其中 GPDispatcherServlet 是有自己模拟 Spring 实现的核心功能类

自定义 Annotation

@GPService 注解:

package com.gupaoedu.mvcframework.annotation;

import java.lang.annotation.*;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GPService {
    String value() default "";
}

@GPAutowired 注解:

package com.gupaoedu.mvcframework.annotation;

import java.lang.annotation.*;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GPAutowired {
    String value() default "";
}

@GPController 注解:

package com.gupaoedu.mvcframework.annotation;

import java.lang.annotation.*;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GPController {
    String value() default "";
}

@GPRequestMapping 注解:

package com.gupaoedu.mvcframework.annotation;

import java.lang.annotation.*;

@Target({ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GPRequestMapping {
    String value() default "";
}

@GPRequestParam 注解:

package com.gupaoedu.mvcframework.annotation;

import java.lang.annotation.*;

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME) @Documented
public @interface GPRequestParam {
    String value() default "";
}

配置 Annotation

配置业务实现类 DemoService:

package com.gupaoedu.demo.service.impl;
import com.gupaoedu.demo.service.IDemoService;
import com.gupaoedu.mvcframework.annotation.GPService;

/**
 * 核心业务逻辑
 */
@GPService public class DemoService implements IDemoService{
    public String get(String name) {
        return "My name is " + name;
    }
}

配置请求入口类 DemoAction:

package com.zhunongyun.toalibaba.security.utils;

import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.gupaoedu.demo.service.IDemoService;
import com.gupaoedu.mvcframework.annotation.GPAutowired;
import com.gupaoedu.mvcframework.annotation.GPController;
import com.gupaoedu.mvcframework.annotation.GPRequestMapping;
import com.gupaoedu.mvcframework.annotation.GPRequestParam;

@GPController
@GPRequestMapping("/demo")
public class DemoAction {
    @GPAutowired
    private IDemoService demoService;

    @GPRequestMapping("/query")
    public void query(HttpServletRequest req, HttpServletResponse resp,
        @GPRequestParam("name") String name) {
        String result = demoService.get(name);
        try {
            resp.getWriter().write(result);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @GPRequestMapping("/add")
    public void add(HttpServletRequest req, HttpServletResponse resp,
        @GPRequestParam("a") Integer a, @GPRequestParam("b") Integer b) {
        try {
            resp.getWriter().write(a + "+" + b + "=" + (a + b));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @GPRequestMapping("/remove")
    public void remove(HttpServletRequest req, HttpServletResponse resp,
        @GPRequestParam("id") Integer id) {
    }
}

至此,配置阶段就已经完成

容器初始化

实现 V 1 版本

所有的核心逻辑全部写在一个 init()方法中

package com.gupaoedu.mvcframework.v 1.servlet;

import com.gupaoedu.mvcframework.annotation.*;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;

public class GPDispatcherServlet extends HttpServlet {

    private Map<String, Object> mapping = new HashMap<String, Object>();

    private Map<String, Object> map = new HashMap<>();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            this.doDispatch(req, resp);
        } catch (Exception e) {
            resp.getWriter().write("500 Exception " + Arrays.toString(e.getStackTrace()));
        }
    }

    private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        String url = req.getRequestURI();
        String contextPath = req.getContextPath();
        url = url.replace(contextPath, "").replaceAll("/+", "/");
        if (!this.mapping.containsKey(url)) {
            resp.getWriter().write("404 Not Found!!");
            return;
        }
        Method method = (Method) this.mapping.get(url);
        Map<String, String[]> params = req.getParameterMap();
        method.invoke(this.mapping.get(method.getDeclaringClass().getName()), new Object[]{req, resp, params.get("name")[0]});
    }

    @Override
    public void init(ServletConfig config) throws ServletException {

        try (InputStream is = this.getClass().getClassLoader().getResourceAsStream(
                config.getInitParameter("contextConfigLocation"))) {
            Properties configContext = new Properties();

            configContext.load(is);
            String scanPackage = configContext.getProperty("scanPackage");

            this.doScanner(scanPackage);

            // 读取代码上的注解,将 Controller,Service 以及 requestURL 作为 key,
            // 将对应的类和方法信息作为 value,保存到 map 中

            mapping.putAll(map);
            for (String className : map.keySet()) {
                if (!className.contains(".")) {
                    continue;
                }
                Class<?> clazz = Class.forName(className);
                if (clazz.isAnnotationPresent(GPController.class)) {
                    mapping.put(className, clazz.newInstance());
                    String baseUrl = "";
                    if (clazz.isAnnotationPresent(GPRequestMapping.class)) {
                        GPRequestMapping requestMapping = clazz.getAnnotation(GPRequestMapping.class);
                        baseUrl = requestMapping.value();
                    }
                    Method[] methods = clazz.getMethods();
                    for (Method method : methods) {
                        if (!method.isAnnotationPresent(GPRequestMapping.class)) {
                            continue;
                        }
                        GPRequestMapping requestMapping = method.getAnnotation(GPRequestMapping.class);
                        String url = (baseUrl + "/" + requestMapping.value()).replaceAll("/+", "/");
                        mapping.put(url, method);
                        System.out.println("Mapped " + url + "," + method);
                    }
                } else if (clazz.isAnnotationPresent(GPService.class)) {
                    GPService service = clazz.getAnnotation(GPService.class);
                    String beanName = service.value();
                    if ("".equals(beanName)) {
                        beanName = clazz.getName();
                    }
                    Object instance = clazz.newInstance();
                    mapping.put(beanName, instance);
                    for (Class<?> i : clazz.getInterfaces()) {
                        mapping.put(i.getName(), instance);
                    }
                } else {
                    continue;
                }
            }

            // 将 service 注入到 controller 中
            for (Object object : mapping.values()) {
                if (object == null) {
                    continue;
                }
                Class clazz = object.getClass();
                if (clazz.isAnnotationPresent(GPController.class)) {
                    Field[] fields = clazz.getDeclaredFields();
                    for (Field field : fields) {
                        if (!field.isAnnotationPresent(GPAutowired.class)) {
                            continue;
                        }
                        GPAutowired autowired = field.getAnnotation(GPAutowired.class);
                        String beanName = autowired.value();
                        if ("".equals(beanName)) {
                            beanName = field.getType().getName();
                        }
                        field.setAccessible(true);
                        try {
                            field.set(mapping.get(clazz.getName()), mapping.get(beanName));
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        map.clear();
        System.out.print("GP MVC Framework is init");
    }

    /**
     * 扫描 scanPackage=com.gupaoedu.demo 目录下所有的 class 文件
     *
     * @param scanPackage
     */
    private void doScanner(String scanPackage) {

        URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.", "/"));
        File classDir = new File(url.getFile());
        for (File file : classDir.listFiles()) {
            if (file.isDirectory()) {
                doScanner(scanPackage + "." + file.getName());
            } else {
                if (!file.getName().endsWith(".class")) {
                    continue;
                }
                String clazzName = (scanPackage + "." + file.getName().replace(".class", ""));
                map.put(clazzName, null);
            }
        }
    }
}

实现 V 2 版本

在 V 1 版本上进了优化,采用了常用的设计模式(工厂模式,单例模式,委派模式,策略模式),将 init()方法中的代码进行封装.按照之前的实现思路,先搭基础框架,再填肉注血,具体代码如下:

//初始化阶段
@Override
public void init(ServletConfig config) throws ServletException {
    //模板模式
    //1、加载配置文件
    this.doLoadConfig(config.getInitParameter("contextConfigLocation"));
    //2、扫描相关的类
    this.doScanner(contextConfig.getProperty("scanPackage"));
    //3、初始化所有相关的类的实例,并且放入到 IOC 容器之中
    this.doInstance();
    //4、完成依赖注入
    this.doAutowired();
    //5、初始化 HandlerMapping
    this.initHandlerMapping();

    System.out.println("GP Spring framework is init.");
}

声明全局的成员变量,其中 IOC 容器就是注册时单例的具体案例:

// 存储 aplication.properties 的配置内容
private Properties contextConfig = new Properties();
//存储所有扫描到的类
private List<String> classNames = new ArrayList<String>();
//IOC 容器,保存所有实例化对象
//注册式单例模式
private Map<String, Object> ioc = new HashMap<String, Object>();
//保存 Controller 中所有 Mapping 的对应关系
private Map<String, Method> handlerMapping = new HashMap<String, Method>();

实现 doLoadConfig()方法:

private void doLoadConfig(String contextConfigLocation) {
    try (InputStream fis = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation)) {
        //1、读取配置文件
        contextConfig.load(fis);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

实现 doScanner()方法:

private void doScanner(String scanPackage) {
    //包传过来包下面的所有的类全部扫描进来的
    URL url = this.getClass().getClassLoader()
            .getResource("/" + scanPackage.replaceAll("\\.", "/"));
    File classPath = new File(url.getFile());

    for (File file : classPath.listFiles()) {
        if (file.isDirectory()) {
            doScanner(scanPackage + "." + file.getName());
        } else {
            if (!file.getName().endsWith(".class")) {
                continue;
            }
            String className = (scanPackage + "." + file.getName()).replace(".class", "");
            classNames.add(className);
        }
    }
}

实现 doInstance()方法,doInstance()方法就是工厂模式的具体实现:

//控制反转过程
//工厂模式来实现的
private void doInstance() {
    if (classNames.isEmpty()) {
        return;
    }

    try {
        for (String className : classNames) {
            Class<?> clazz = Class.forName(className);

            if (clazz.isAnnotationPresent(GPController.class)) {
                Object instance = clazz.newInstance();
                String beanName = toLowerFirstCase(clazz.getSimpleName());
                ioc.put(beanName, instance);
            } else if (clazz.isAnnotationPresent(GPService.class)) {
                //1、默认的类名首字母小写
                String beanName = toLowerFirstCase(clazz.getSimpleName());
                //2、自定义命名
                GPService service = clazz.getAnnotation(GPService.class);
                if (!"".equals(service.value())) {
                    beanName = service.value();
                }
                Object instance = clazz.newInstance();
                ioc.put(beanName, instance);
                //3、根据类型注入实现类,投机取巧的方式
                for (Class<?> i : clazz.getInterfaces()) {
                    if (ioc.containsKey(i.getName())) {
                        throw new Exception("The beanName is exists!!");
                    }
                    ioc.put(i.getName(), instance);
                }
            } else {
                continue;
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    classNames.clear();
}

为了处理方便,自己实现了 toLowerFirstCase 方法,来实现类名首字母小写,具体代码如下:

private String toLowerFirstCase(String simpleName) {
    char[] chars = simpleName.toCharArray();
    chars[0] += 32;
    return String.valueOf(chars);
}

实现 doAutowired()方法:

private void doAutowired() {
    if (ioc.isEmpty()) {
        return;
    }
    for (Map.Entry<String, Object> entry : ioc.entrySet()) {
        //拿到实例对象中的所有属性
        Field[] fields = entry.getValue().getClass().getDeclaredFields();
        for (Field field : fields) {
            if (!field.isAnnotationPresent(GPAutowired.class)) {
                continue;
            }
            GPAutowired autowired = field.getAnnotation(GPAutowired.class);
            String beanName = autowired.value().trim();
            if ("".equals(beanName)) {
                beanName = field.getType().getName();
            }
            //不管你愿不愿意,强吻
            //设置私有属性的访问权限
            field.setAccessible(true);
            try {
                //执行注入动作
                field.set(entry.getValue(), ioc.get(beanName));
            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
    }
}

实现 initHandlerMapping()方法,handlerMapping 就是策略模式的应用案例:

private void initHandlerMapping() {
    if (ioc.isEmpty()) {
        return;
    }

    for (Map.Entry<String, Object> entry : ioc.entrySet()) {
        Class<?> clazz = entry.getValue().getClass();
        if (!clazz.isAnnotationPresent(GPController.class)) {
            continue;
        }

        String baseUrl = "";
        //获取 Controller 的 url 配置
        if (clazz.isAnnotationPresent(GPRequestMapping.class)) {
            GPRequestMapping requestMapping = clazz.getAnnotation(GPRequestMapping.class);
            baseUrl = requestMapping.value();
        }

        //获取 Method 的 url 配置
        Method[] methods = clazz.getMethods();
        for (Method method : methods) {

            //没有加 RequestMapping 注解的直接忽略
            if (!method.isAnnotationPresent(GPRequestMapping.class)) {
                continue;
            }

            //映射 URL
            GPRequestMapping requestMapping = method.getAnnotation(GPRequestMapping.class);
            //  /demo/query
            //  (//demo//query)
            String url = ("/" + baseUrl + "/" + requestMapping.value())
                    .replaceAll("/+", "/");
            handlerMapping.put(url, method);
            System.out.println("Mapped " + url + "," + method);
        }
    }
}

到这里位置初始化阶段就已经完成,接下实现运行阶段的逻辑,来看 doPost/doGet 的代码:

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    this.doPost(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    //派遣,分发任务
    try {
        //委派模式
        this.doDispatch(req, resp);
    } catch (Exception e) {
        e.printStackTrace();
        resp.getWriter().write("500 Excetion Detail:" + Arrays.toString(e.getStackTrace()));
    }
}

doPost()方法中,用了委派模式,委派模式的具体逻辑在 doDispatch()方法中:

private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
    String url = req.getRequestURI();
    String contextPath = req.getContextPath();
    url = url.replaceAll(contextPath, "").replaceAll("/+", "/");
    if (!this.handlerMapping.containsKey(url)) {
        resp.getWriter().write("404 Not Found!!");
        return;
    }

    Method method = this.handlerMapping.get(url);
    //第一个参数:方法所在的实例
    //第二个参数:调用时所需要的实参
    Map<String, String[]> params = req.getParameterMap();
    //获取方法的形参列表
    Class<?>[] parameterTypes = method.getParameterTypes();
    //保存请求的 url 参数列表
    Map<String, String[]> parameterMap = req.getParameterMap();
    //保存赋值参数的位置
    Object[] paramValues = new Object[parameterTypes.length];
    //按根据参数位置动态赋值
    for (int i = 0; i < parameterTypes.length; i++) {
        Class parameterType = parameterTypes[I];
        if (parameterType == HttpServletRequest.class) {
            paramValues[i] = req;
            continue;
        } else if (parameterType == HttpServletResponse.class) {
            paramValues[i] = resp;
            continue;
        } else if (parameterType == String.class) {
            //提取方法中加了注解的参数
            Annotation[][] pa = method.getParameterAnnotations();
            for (int j = 0; j < pa.length; j++) {
                for (Annotation a : pa[i]) {
                    if (a instanceof GPRequestParam) {
                        String paramName = ((GPRequestParam) a).value();
                        if (!"".equals(paramName.trim())) {
                            String value = Arrays.toString(parameterMap.get(paramName))
                                    .replaceAll("\\[|\\]", "")
                                    .replaceAll("\\s", ",");
                            paramValues[i] = value;
                        }
                    }
                }
            }

        }
    }
    //投机取巧的方式
    //通过反射拿到 method 所在 class,拿到 class 之后还是拿到 class 的名称
    //再调用 toLowerFirstCase 获得 beanName
    String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
    method.invoke(ioc.get(beanName), new Object[]{req, resp, params.get("name")[0]});
}

实现 V 3 版本

在 V 2 版本中,基本功能以及完全实现,但代码的优雅程度还不如人意.譬如 HandlerMapping 还不能像 SpringMVC 一样支持正则,url 参数还不支持强制类型转换,在反射调用前还需要重新获取 beanName,在 V 3 版本中,下面我们继续优化.
首先,改造 HandlerMapping,在真实的 Spring 源码中,HandlerMapping 其实是一个 List 而非 Map.List 中的元素是一个自定义的类型.现在我们来仿真写一段代码,先定义一个内部类 Handler 类:

/**
 * Handler 记录 Controller 中的 RequestMapping 和 Method 的对应关系
 *
 * @author Tom
 * 内部类
 */
private class Handler {

    protected Object controller;    //保存方法对应的实例
    protected Method method;        //保存映射的方法
    protected Pattern pattern;
    protected Map<String, Integer> paramIndexMapping;    //参数顺序

    /**
     * 构造一个 Handler 基本的参数
     *
     * @param controller
     * @param method
     */
    protected Handler(Pattern pattern, Object controller, Method method) {
        this.controller = controller;
        this.method = method;
        this.pattern = pattern;

        paramIndexMapping = new HashMap<String, Integer>();
        putParamIndexMapping(method);
    }

    private void putParamIndexMapping(Method method) {

        //提取方法中加了注解的参数
        Annotation[][] pa = method.getParameterAnnotations();
        for (int i = 0; i < pa.length; i++) {
            for (Annotation a : pa[i]) {
                if (a instanceof GPRequestParam) {
                    String paramName = ((GPRequestParam) a).value();
                    if (!"".equals(paramName.trim())) {
                        paramIndexMapping.put(paramName, i);
                    }
                }
            }
        }

        //提取方法中的 request 和 response 参数
        Class<?>[] paramsTypes = method.getParameterTypes();
        for (int i = 0; i < paramsTypes.length; i++) {
            Class<?> type = paramsTypes[I];
            if (type == HttpServletRequest.class ||
                    type == HttpServletResponse.class) {
                paramIndexMapping.put(type.getName(), i);
            }
        }
    }
}

然后,优化 HandlerMapping 的结构,代码如下:

//保存所有的 Url 和方法的映射关系
private List<Handler> handlerMapping = new ArrayList<>();

修改 initHandlerMapping()方法:

private void initHandlerMapping(){
    if(ioc.isEmpty()){ return; }

    for (Entry<String, Object> entry : ioc.entrySet()) {
        Class<?> clazz = entry.getValue().getClass();
        if(!clazz.isAnnotationPresent(GPController.class)){ continue; }

        String url = "";
        //获取 Controller 的 url 配置
        if(clazz.isAnnotationPresent(GPRequestMapping.class)){
            GPRequestMapping requestMapping = clazz.getAnnotation(GPRequestMapping.class);
            url = requestMapping.value();
        }

        //获取 Method 的 url 配置
        Method [] methods = clazz.getMethods();
        for (Method method : methods) {
            //没有加 RequestMapping 注解的直接忽略
            if(!method.isAnnotationPresent(GPRequestMapping.class)){ continue; }

            //映射 URL
            GPRequestMapping requestMapping = method.getAnnotation(GPRequestMapping.class);
            String regex = ("/" + url + requestMapping.value()).replaceAll("/+", "/");
            Pattern pattern = Pattern.compile(regex);
            handlerMapping.add(new Handler(pattern,entry.getValue(),method));
            System.out.println("mapping " + regex + "," + method);
        }
    }
}

修改 doDispatch()方法:

private void doDispatch(HttpServletRequest req,HttpServletResponse resp) throws Exception{
    try{
        Handler handler = getHandler(req);

        if(handler == null){
            //如果没有匹配上,返回404错误
            resp.getWriter().write("404 Not Found");
            return;
        }

        //获取方法的参数列表
        Class<?> [] paramTypes = handler.method.getParameterTypes();

        //保存所有需要自动赋值的参数值
        Object [] paramValues = new Object[paramTypes.length];

        Map<String,String[]> params = req.getParameterMap();
        for (Entry<String, String[]> param : params.entrySet()) {
            String value = Arrays.toString(param.getValue()).replaceAll("\\[|\\]", "").replaceAll(",\\s", ",");

            //如果找到匹配的对象,则开始填充参数值
            if(!handler.paramIndexMapping.containsKey(param.getKey())){continue;}
            int index = handler.paramIndexMapping.get(param.getKey());
            paramValues[index] = convert(paramTypes[index],value);
        }

        //设置方法中的 request 和 response 对象
        int reqIndex = handler.paramIndexMapping.get(HttpServletRequest.class.getName());
        paramValues[reqIndex] = req;
        int respIndex = handler.paramIndexMapping.get(HttpServletResponse.class.getName());
        paramValues[respIndex] = resp;

        handler.method.invoke(handler.controller, paramValues);
    }catch(Exception e){
        throw e;
    }
}

在以上代码中,增加了两个方法,一个是 getHandler()方法,主要负责处理 url 的正则匹配;一个是 convert()方法,主要负责 url 参数的强制类型转换.至此,手写 Mini 版 SpringMVC 框架就已全部完成.

运行效果演示

在浏览器输入:http://localhost:8080/demo/query.json?name=Tom,就会得到下面的结果:

image

资源信息

涉及的代码 github 地址:https://github.com/huaweirookie/toalibaba/tree/master/spring/gupaoedu-vip-spring-1.0

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