一. AOP,拦截器,异步队列

系列文章中的大章节编号为代码模块号

结构

一. aspect

1.1 LogAspect

@Aspect
@Component
public class LogAspect {
    private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);

    @Before("execution(* com.nowcoder.controller.*Controller.*(..))")
    public void beforeMethod(JoinPoint joinPoint) {
        StringBuilder sb = new StringBuilder();
        for (Object arg : joinPoint.getArgs()) {
            sb.append("arg:" + arg.toString() + "|");
        }
        logger.info("before method: " + sb.toString());
    }

    @After("execution(* com.nowcoder.controller.IndexController.*(..))")
    public void afterMethod(JoinPoint joinPoint) {
        logger.info("after method: ");
    }
}

1) @Component

使用@Component标记为IOC容器中的组件

2) Logger LOG = LoggerFactory.getLogger()

用Logger工厂获取Logger实例

3) JoinPoint 对象

JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象.
常用api:

方法名 功能
Signature getSignature() 获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息
Object[] getArgs() 获取传入目标方法的参数对象
Object getTarget() 获取被代理的对象
Object getThis() 获取代理对象

2. async

async

异步点赞

为什么异步

点赞,回复评论的时候,表面上是赞数增加了,其实还有很多其他的工作要做。比如,对方要收到消息提醒,成就值增加。一些行为会引起一系列连锁反应。如果在点赞时立马处理,会影响程序运行效率。而且会造成代码冗余,比如发布新闻,和回复评论都可以使得成就值增加,如果都跟着写在后面的代码里会把成就值增加这段代码写两遍,所以大型服务需要服务化和异步化。

  • 服务化
    服务化:某一个单独的业务独立成一个工程,提供接口。不只是service层的一个类。
    暴露一些接口,比如数据库服务,如果一个部门要去数据库查询,小公司可能写个SQL语句。对于大公司,需要一个组单独做数据库服务,暴露接口给别的部门用。好处是防止别的部门有数据库权限,数据库单独有一个组维护,出问题找他们运维就好。
  • 异步化
    异步化:用户点赞,用户首先要知道的是这个赞已经点上了。用户提交Oj,用户要立马知道的是代码有没有通过。而后面应该的东西,比如积分增加了,用户不会有立马想知道的需求,如果间隔几秒钟在更新,用户也不会有很大的意见。

概述

在一个网站中,一个业务发生的同时,还有一些后续业务需要发生。

比如点赞,除了完成点赞功能外,还有一系列,比如提醒被点赞的用户等等,为了能够实现这些操作并且不拖慢单纯点赞功能的实现,我们将这些使用异步队列实现。

处理流程如下图:

处理流程
  • Biz(生产)
    Biz为业务部门,理解为点赞的实现,也就是在实现点赞的同时通过EventProducer发送一个事件
  • 进入队列
    这个事件进入队列等待,队列另一头,有一个EventConsumer,不断消费事件
  • EventHandler(消费)
    EventConsumer下面有很多EventHandler,只要EventHandler发现自己需要处理的事件类型,就会进行相应的操作。

优点:①后续业务的实现,不会拖慢主业务。②如果后续业务的服务器挂掉,只要重启,继续从优先队列消费事件即可。

2.1 LikeHandler

记得开启Rrdis---redis-server.exe redis.windows.conf

具体的执行动作,具体的实现类,这个是点赞后要执行的行为,给别人发提醒。

@Component
public class LikeHandler implements EventHandler {
    @Autowired
    MessageService messageService;

    @Autowired
    UserService userService;

    @Override
    public void doHandle(EventModel model) {
        Message message = new Message();
        message.setFromId(3);
        //message.setToId(model.getEntityOwnerId());
        message.setToId(model.getActorId());
        User user = userService.getUser(model.getActorId());
        message.setContent("用户" + user.getName()
                + "赞了你的资讯,http://127.0.0.1:8080/news/" + model.getEntityId());
        message.setCreatedDate(new Date());
        messageService.addMessage(message);
    }

    @Override
    public List<EventType> getSupportEventTypes() {
        return Arrays.asList(EventType.LIKE);
    }
}

1)参考 Spring@Autowired注解与自动装

Spring 2.5 引入了 @Autowired 注释,它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。 通过 @Autowired的使用来消除 set ,get方法。

  1. 事先在 Spring 容器中声明
    Spring 通过一个 BeanPostProcessor 对 @Autowired 进行解析,所以要让 @Autowired 起作用必须事先在 Spring 容器中声明 AutowiredAnnotationBeanPostProcessor Bean。
<!-- 该 BeanPostProcessor 将自动对标注 @Autowired 的 Bean 进行注入 -->     
  <bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor"/> 
  1. 修改在原来注入spirng容器中的bean的方法
    修改在原来注入spirng容器中的bean的方法:在域变量上加上标签@Autowired,并且去掉 相应的get 和set方法

  2. <porpery >也去掉
    在applicatonContext.xml中 把原来 引用的<porpery >标签也去掉。

2) Date

java.util 包提供了 Date 类来封装当前的日期和时间。 Date 类提供两个构造函数来实例化 Date 对象。

  • 第一个构造函数使用当前日期和时间来初始化对象。
    Date( )
  • 第二个构造函数接收一个参数,该参数是从1970年1月1日起的毫秒数。
    Date(long millisec)
方法 作用
boolean after(Date date) 若当调用此方法的Date对象在指定日期之后返回true,否则返回false
boolean before(Date date) 若当调用此方法的Date对象在指定日期之前返回true,否则返回false。
long getTime( ) 返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数。
String toString( ) 把此 Date 对象转换为以下形式的 String: dow mon dd hh:mm:ss zzz yyyy 其中: dow 是一周中的某一天 (Sun, Mon, Tue, Wed, Thu, Fri, Sat)。

3)Arrays.asList

Arrays.asList使用指南

List 是一种很有用的数据结构,如果需要将一个数组转换为 List 以便进行更丰富的操作的话,可以这么实现:

String[] myArray = { "Apple", "Banana", "Orange" }; 
List<String> myList = Arrays.asList(myArray);

或者

List<String> myList = Arrays.asList("Apple", "Orange");

上面这两种形式都是十分常见的:将需要转化的数组作为参数,或者直接把数组元素作为参数,都可以实现转换。

2.2 LoginExceptionHandler

@Component
public class LoginExceptionHandler implements EventHandler {
    @Autowired
    MessageService messageService;

    @Autowired
    MailSender mailSender;

    @Override
    public void doHandle(EventModel model) {
        // 判断是否有异常登陆
        Message message = new Message();
        message.setToId(model.getActorId());
        message.setContent("你上次的登陆ip异常");
        message.setFromId(3);
        message.setCreatedDate(new Date());
        messageService.addMessage(message);
    //邮件发送三
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("username", model.getExt("username"));
        mailSender.sendWithHTMLTemplate(model.getExt("email"), "登陆异常", "mails/welcome.html",
                map);
    }

    @Override
    public List<EventType> getSupportEventTypes() {
        return Arrays.asList(EventType.LOGIN);
    }
}

2.3 EventConsumer

解决的问题

如何将活动分发下去给相关的所有handle实现。

步骤

消费活动,在初始化前,先得到Handler接口所有的实现类,遍历实现类。
通过getSupportEventType得到每个实现类对应处理的活动类型。反过来记录在config哈希表中,config中的key是活动的类型,比如说是LIKE,COMMENT,是枚举里的成员,value是一个ArrayList的数组,里面存放的是各种实现方法。见代码中的。当从队列中获得一个活动时,这里用的是从右向外pop()一个活动实体。进行解析。这里的config.get(eventModel.getType())是一个数组,里面存放着所有关于这个活动要执行的实现类。遍历这个数组,开始执行实现类里的方法。

一些注释

  1. implements InitializingBean , @Override afterPropertiesSet()
    InitializingBean 通过实现此接口的afterPropertiesSet()方法记录哪些Event需要哪些handler来处理
  2. implements ApplicationContextAware
    ApplicationContextAware 通过实现此接口的setApplicationContext方法获取上下文
  3. Map<EventType, List<EventHandler>> config = new HashMap<>();
    用来存储一个事件类型对应的所有的eventhandler,下次有该事件产生时,即可直接调用对应的list
  4. Map<String, EventHandler> beans = applicationContext.getBeansOfType(EventHandler.class);
    找出上下文中所有实现了EventHandler接口的类,存入beans
  5. if (beans != null)……
    遍历所有的handler,将他们存入他们所监听的eventType对应的list中
  6. List<String> messages = jedisAdapter.brpop(0, key);
    从redis的队列中取出事件,并存入list中
  7. for (String message : messages)
    遍历取出的事件
    7.1 if (message.equals(key))
    第一个元素是队列名字,跳过
    7.2 if (!config.containsKey(eventModel.getType()))
    跳过不能处理的事件
    7.3 for (EventHandler handler : config.get(eventModel.getType()))
    处理他的所有的handler
@Service
//1------------------------
public class EventConsumer implements InitializingBean, ApplicationContextAware {

    private static final Logger logger = LoggerFactory.getLogger(EventConsumer.class);
    //2--------------
    private Map<EventType, List<EventHandler>> config = new HashMap<>();
    private ApplicationContext applicationContext;

    @Autowired
    private JedisAdapter jedisAdapter;

    @Override
    public void afterPropertiesSet() throws Exception {
        //4----------------------
        Map<String, EventHandler> beans = applicationContext.getBeansOfType(EventHandler.class);
        //5-----------------------------
        if (beans != null) {
            for (Map.Entry<String, EventHandler> entry : beans.entrySet()) {
                List<EventType> eventTypes = entry.getValue().getSupportEventTypes();//查看事件的监视事件
                for (EventType type : eventTypes) {
                    if (!config.containsKey(type)) {
                        config.put(type, new ArrayList<EventHandler>());
                        System.out.println("添加一个新的 :" + type);//20180802
                    }
                    config.get(type).add(entry.getValue());// 注册每个事件的处理函数
                    System.out.println("注册每个事件的处理函数 :" + type + " " + entry.getValue()); //20180802
                }
            }
        }

        Thread thread = new Thread(new Runnable() {// 启动线程去消费事件
            @Override
            public void run() {
                while (true) {// 从队列一直消费
                    String key = RedisKeyUtil.getEventQueueKey();
                    //6------------------------------
                    List<String> messages = jedisAdapter.brpop(0, key);
                    
                    for (String message : messages) {
                        //7.1---------------
                        if (message.equals(key)) {
                            continue;
                        }
                        EventModel eventModel = JSON.parseObject(message, EventModel.class);
                        //7.2---------------
                        System.out.println("找到这个事件的处理handler列表 : " + eventModel.getType()) //20180802
                        if (!config.containsKey(eventModel.getType())) { //找到这个事件的处理handler列表
                            logger.error("不能识别的事件");
                            continue;
                        }
                        //7.3---------------
                        for (EventHandler handler : config.get(eventModel.getType())) {//处理他的所有的handler
                            System.out.println("处理事件 : " + eventModel.getType() + " " + handler.getClass() );//20180802
                            handler.doHandle(eventModel);
                        }
                    }
                }
            }
        });
        thread.start();
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

1)Jedis

Jedis

BLPOP key [key ...] timeout

BLPOP 是列表的阻塞式(blocking)弹出原语。
它是 LPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BLPOP 命令阻塞,直到等待超时或发现可弹出元素为止。
当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的头元素。
非阻塞行为:
当 BLPOP 被调用时,如果给定 key 内至少有一个非空列表,那么弹出遇到的第一个非空列表的头元素,并和被弹出元素所属的列表的名字一起,组成结果返回给调用者。
当存在多个给定 key 时, BLPOP 按给定 key 参数排列的先后顺序,依次检查各个列表。
假设现在有 job 、 command 和 request 三个列表,其中 job 不存在, command 和 request 都持有非空列表。考虑以下命令:BLPOP job command request 0

超时参数 timeout 接受一个以秒为单位的数字作为值。超时参数设为 0 表示阻塞时间可以无限期延长(block indefinitely) 。

BRPOP key [key ...] timeout

它是 RPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BRPOP 命令阻塞,直到等待超时或发现可弹出元素为止。
BRPOP 除了弹出元素的位置和 BLPOP 不同之外,其他表现一致。

2)Map.Entry

Map.Entry使用详解

Map.Entry是Map声明的一个内部接口,此接口为泛型,定义为Entry<K,V>。它表示Map中的一个实体(一个key-value对)。接口中有getKey(),getValue方法。

3)Fastjson

Fastjson API

2.4 EventHandler

设计为一个接口,handler都实现此接口。

public interface EventHandler {
    void doHandle(EventModel model);//处理此事件
    List<EventType> getSupportEventTypes();//添加监视的事件类型
}

2.5 EventModel

即发送的队列的事件模型,只有一些基本属性和get、set方法。

其中一些set的return 设置为this,是因为方便连续set多个属性。

public class EventModel {
    private EventType type;
    private int actorId;
    private int entityType;
    private int entityId;
    private int entityOwnerId;
    private Map<String, String> exts = new HashMap<String, String>();

    public EventModel(EventType type) {
        this.type = type;
    }
    public EventModel() {
    }

    public String getExt(String key) {
        return exts.get(key);
    }
    public EventModel setExt(String key, String value) {
        exts.put(key, value);
        return this;//方便连续set多个属性。
    }

    public EventType getType() {
        return type;
    }
    public EventModel setType(EventType type) {
        this.type = type;
        return this;
    }

    public int getActorId() {
        return actorId;
    }
    public EventModel setActorId(int actorId) {
        this.actorId = actorId;
        return this;
    }

    public int getEntityType() {
        return entityType;
    }
    public EventModel setEntityType(int entityType) {
        this.entityType = entityType;
        return this;
    }

    public int getEntityId() {
        return entityId;
    }
    public EventModel setEntityId(int entityId) {
        this.entityId = entityId;
        return this;
    }

    public int getEntityOwnerId() {
        return entityOwnerId;
    }
    public EventModel setEntityOwnerId(int entityOwnerId) {
        this.entityOwnerId = entityOwnerId;
        return this;
    }

    public Map<String, String> getExts() {
        return exts;
    }
    public void setExts(Map<String, String> exts) {
        this.exts = exts;
    }
}

2.6 EventProducer

活动生产者,相当于生产消费者中的生产者,在controller层执行一个动作后,用这个类把需要异步的信息打包好,放进Redis的队列中。放入是把EventModel序列化为JSON,存入Redis的列表中。

@Service
public class EventProducer {
    @Autowired
    JedisAdapter jedisAdapter;

    public boolean fireEvent(EventModel model) {
        try {
            String json = JSONObject.toJSONString(model);
            String key = RedisKeyUtil.getEventQueueKey();
            jedisAdapter.lpush(key, json);
            System.out.println("产生一个异步事件:" + eventModel.getType());//20180802
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

1) JSON

由于 JSON 语法是 JavaScript 语法的子集,JavaScript 函数 eval() 可用于将 JSON 文本转换为 JavaScript 对象。

概览

  • JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。
  • JSON 是存储和交换文本信息的语法。类似 XML。
  • JSON 比 XML 更小、更快,更易解析。

为什么使用 JSON?

对于 AJAX 应用程序来说,JSON 比 XML 更快更易使用:

使用 XML
  • 读取 XML 文档
  • 使用 XML DOM 来循环遍历文档
  • 读取值并存储在变量中
使用 JSON
  • 读取 JSON 字符串
  • eval() 处理 JSON 字符串

书写格式

JSON 数据的书写格式是:名称/值对。
名称/值对包括字段名称(在双引号中),后面写一个冒号,然后是值:
“firstName” : “John”
这很容易理解,等价于这条 JavaScript 语句:
firstName = “John”

json嵌套

对于json嵌套,只要记住

  • 符号”前是键,符号后是值
  • 大括号成对找

一层层剥开,就清楚了。

2)前后台的传输

  • JSON.parseObject,是将Json字符串转化为相应的对象
  • JSON.toJSONString则是将对象转化为Json字符串。

2.7 EventType

EventType 是获得活动的类型,可以有点赞,评论,登陆等待

public enum EventType {
    LIKE(0),
    COMMENT(1),
    LOGIN(2),
    MAIL(3);

    private int value;

    EventType(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

3. configuration

3.1. ToutiaoWebConfiguration

@Component
public class ToutiaoWebConfiguration extends WebMvcConfigurerAdapter {
    @Autowired
    PassportInterceptor passportInterceptor;

    @Autowired
    LoginRequiredInterceptor loginRequiredInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(passportInterceptor);
        registry.addInterceptor(loginRequiredInterceptor).
                addPathPatterns("/msg/*").addPathPatterns("/like").addPathPatterns("/dislike");
        super.addInterceptors(registry);
    }
}

1)spring boot中使用拦截器

1、注册拦截器

创建一个类MyWebConfig继承WebMvcConfigurerAdapter,并重写addInterceptors方法

多个拦截器组成一个拦截器链

  • addPathPatterns
    添加拦截规则
  • excludePathPatterns
    排除拦截
@Configuration
public class MyWebConfig extends WebMvcConfigurerAdapter {
    @Autowired
    MyiInterceptor myiInterceptor;

    @Override    //注册 拦截器
    public void addInterceptors(InterceptorRegistry registry) {
//拦截器myiInterceptor只拦截'/111'的请求,不拦截'/helloWorld'      
        registry.addInterceptor(myiInterceptor).addPathPatterns("/111")
                          .excludePathPatterns("/helloWorld");
        super.addInterceptors(registry);
    }
}

2、自定义拦截器

创建一个自定义拦截器MyiInterceptor实现HandlerInterceptor接口重写所有的方法实现自己的业务

6. interceptor

interceptor

6.1 LoginRequiredInterceptor

@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {

    @Autowired
    private HostHolder hostHolder;

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        if (hostHolder.getUser() == null) {
            httpServletResponse.sendRedirect("/?pop=1");
            return false;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
    }
}

1)拦截器(Interceptor)

作用

在Web开发中,拦截器(Interceptor)可以用来验证是否登录预先设置数据以及统计方法的执行效率等。

分类

Spring中的拦截器分两种,一是HandlerInterceptor,另一个是MethodInterceptor。这里主要说以下HandlerInterceptor。

HandlerInterceptor是SpringMVC项目中的拦截器,拦截目标是请求的地址,比MethodInterceptor先执行。实现一个HandlerInterceptor拦截器可以直接实现该接口,也可以继承HandlerInterceptorAdapter类。

SpringMVC处理请求过程

SpringMVC处理请求的整个过程是

  1. 先根据请求找到对应的HandlerExecutionChain,它包含了处理请求的handler和所有的HandlerInterceptor拦截器
  2. 然后在调用hander之前分别调用每个HandlerInterceptor拦截器preHandle方法
    2.1 若有一个拦截器返回false,则会调用triggerAfterCompletion方法,并且立即返回不再往下执行
    2.2 若所有的拦截器全部返回true并且没有出现异常,则调用handler返回ModelAndView对象;再然后分别调用每个拦截器的postHandle方法;最后,即使是之前的步骤抛出了异常,也会执行triggerAfterCompletion方法。

多拦截器工作流程:

多拦截器工作流程

需要Override的三种方法

(1 )preHandle (HttpServletRequest request, HttpServletResponse response, Object handle)
controller 执行之前调用
该方法将在请求处理之前进行调用。SpringMVC 中的Interceptor 是链式调用的,在一个请求中可以同时存在多个Interceptor 。每个Interceptor 的调用会依据它的声明顺序依次执行,而且最先执行的都是Interceptor 中的preHandle 方法,所以可以在这个方法中进行一些前置初始化操作或者是对当前请求的一个预处理,也可以在这个方法中进行一些判断来决定请求是否要继续进行下去。该方法的返回值是布尔值Boolean 类型的,当它返回为false 时,表示请求结束,后续的Interceptor 和Controller 都不会再执行;当返回值为true 时就会继续调用下一个Interceptor 的preHandle 方法,如果已经是最后一个Interceptor 的时候就会是调用当前请求的Controller 方法。
(2 )postHandle (HttpServletRequest request, HttpServletResponse response, Object handle, ModelAndView modelAndView)
controller 执行之后,且页面渲染之前调用
这个方法包括后面要说到的afterCompletion 方法都只能是在当前所属的Interceptor 的preHandle 方法的返回值为true 时才能被调用。postHandle 方法,是在当前请求进行处理之后,也就是Controller 方法调用之后执行,但是它会在DispatcherServlet 进行视图渲染之前被调用,所以我们可以在这个方法中对Controller 处理之后的ModelAndView 对象进行操作。postHandle 方法被调用的方向跟preHandle 是相反的,也就是说先声明的Interceptor 的postHandle 方法反而会后执行。
(3 )afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex)
页面渲染之后调用,一般用于资源清理操作。
该方法也是需要当前对应的Interceptor 的preHandle 方法的返回值为true 时才会执行。该方法将在整个请求结束之后,也就是在DispatcherServlet 渲染了对应的视图之后执行。这个方法的主要作用是用于进行资源清理工作的。

在Spring Boot中配置拦截器,需要写一个配置类继承WebMvcConfigurerAdapter类并添加该拦截器(见2)

@Component
public class XdtoutiaoWebConfiguration extends WebMvcConfigurerAdapter {
    @Autowired
    PassportInterceptor passportInterceptor;
 
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(passportInterceptor);
        super.addInterceptors(registry);
    }
}

6.2 PassportInterceptor

@Component
public class PassportInterceptor implements HandlerInterceptor {

    @Autowired
    private LoginTicketDAO loginTicketDAO;

    @Autowired
    private UserDAO userDAO;

    @Autowired
    private HostHolder hostHolder;

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        String ticket = null;
        if (httpServletRequest.getCookies() != null) {
            for (Cookie cookie : httpServletRequest.getCookies()) {
                if (cookie.getName().equals("ticket")) {
                    ticket = cookie.getValue();
                    break;
                }
            }
        }

        if (ticket != null) {
            LoginTicket loginTicket = loginTicketDAO.selectByTicket(ticket);
            if (loginTicket == null || loginTicket.getExpired().before(new Date()) || loginTicket.getStatus() != 0) {
                return true;
            }

            User user = userDAO.selectById(loginTicket.getUserId());
            hostHolder.setUser(user);
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
        if (modelAndView != null && hostHolder.getUser() != null) {
            modelAndView.addObject("user", hostHolder.getUser());
        }
    }

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,579评论 18 139
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,289评论 8 265
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,081评论 1 32
  • 北京时间9月13日凌晨1点,全球电子科技业的年度盛事,新一代苹果产品发布会召开。苹果公司终于正式发布了期待已久的i...
    一笑杂谈阅读 332评论 0 4
  • "你们以前遇到不会的选择题开始乱猜答案的时候,会不会犹豫?不知道哪一个是正确答案,或许那个没选的选项,没填满的四方...
    艾莉鲨阅读 283评论 0 2