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

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

一. AOP,拦截器,异步队列_第1张图片
结构

一. 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

一. AOP,拦截器,异步队列_第2张图片
async

异步点赞

为什么异步

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

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

概述

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

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

处理流程如下图:

一. AOP,拦截器,异步队列_第3张图片
处理流程
  • 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 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。
     
   
  1. 修改在原来注入spirng容器中的bean的方法
    修改在原来注入spirng容器中的bean的方法:在域变量上加上标签@Autowired,并且去掉 相应的get 和set方法

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

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 myList = Arrays.asList(myArray);

或者

List 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 map = new HashMap();
        map.put("username", model.getExt("username"));
        mailSender.sendWithHTMLTemplate(model.getExt("email"), "登陆异常", "mails/welcome.html",
                map);
    }

    @Override
    public List 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> config = new HashMap<>();
    用来存储一个事件类型对应的所有的eventhandler,下次有该事件产生时,即可直接调用对应的list
  4. Map beans = applicationContext.getBeansOfType(EventHandler.class);
    找出上下文中所有实现了EventHandler接口的类,存入beans
  5. if (beans != null)……
    遍历所有的handler,将他们存入他们所监听的eventType对应的list中
  6. List 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> config = new HashMap<>();
    private ApplicationContext applicationContext;

    @Autowired
    private JedisAdapter jedisAdapter;

    @Override
    public void afterPropertiesSet() throws Exception {
        //4----------------------
        Map beans = applicationContext.getBeansOfType(EventHandler.class);
        //5-----------------------------
        if (beans != null) {
            for (Map.Entry entry : beans.entrySet()) {
                List eventTypes = entry.getValue().getSupportEventTypes();//查看事件的监视事件
                for (EventType type : eventTypes) {
                    if (!config.containsKey(type)) {
                        config.put(type, new ArrayList());
                        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 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。它表示Map中的一个实体(一个key-value对)。接口中有getKey(),getValue方法。

3)Fastjson

Fastjson API

2.4 EventHandler

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

public interface EventHandler {
    void doHandle(EventModel model);//处理此事件
    List 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 exts = new HashMap();

    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 getExts() {
        return exts;
    }
    public void setExts(Map 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方法。

多拦截器工作流程:

一. AOP,拦截器,异步队列_第4张图片
多拦截器工作流程

需要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();
    }
}

你可能感兴趣的:(一. AOP,拦截器,异步队列)