责任链模式-短链点击行为记录

简介

9月份做了个短信发送的功能,考虑到短信文本字数的限制,需要将原始长链接转换为短链发送,并且需要记录每次的短链点击行为。点击短链之后的处理逻辑主要为:ip黑名单过滤 ->> 短链转换成原始链接 ->>重定向到原始链接->>点击行为记录;就是对同一个请求按照步骤进行链式处理,所以很自然的就想到了要通过责任链模式实现。

责任链模式

定义:为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象持有下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到处理完成为止。

特点

  • 降低了对象之间的耦合度,该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送着和接收者之间无须拥有对方的明确信息。
  • 增强了系统的可拓展性,可以根据需要增加新的请求处理类,满足开闭原则。
  • 增强了为对象指派职责的灵活性,当工作流程发生变化,可以动态地改变链内的成员或者调动它们之间的次序,也可新增或者删除责任。
  • 责任链简化了对象之间的连接,每个对象只需保持一个指向其后继者的引用,不需要保持其他所有处理者的引用。
  • 责任分担:每个处理类只需要关注自己的责任,明确各类的责任范围,符合类的单一职责原则。

常见模式
模式一:

链式处理1.png

在这种模式下,请求上下文沿着处理器传递,每个处理器都有处理请求的机会,直至全部的处理器处理完成后请求结束。
模式二:
链式处理2.png

在这种模式下,请求上下文沿着处理器传递,在每个处理器执行时会首先判断是否满足规则,不满足规则时直接中断处理或者忽略该处理器将请求上下文传递给后继处理器进行处理。
模式三:
偷个懒图就不画了,模式三实际上是对模式一、模式二的优化,该模式下,通过处理器链Chain保存多个处理器并维护它们的处理顺序,该模式下可以动态的增加、删除处理器,具体看下面的示例。

业务场景

短链点击后按照以下步骤进行处理:

  • Ip黑名单过滤:如果当前请求ip在黑名单内,则禁止访问,请求结束。
  • 短链转换:根据短链找到原始长链,如果未找到对应的原始长链,则抛出异常,请求结束。
  • 重定向到原始链接:重定向到原始链接并记录状态
  • 短链点击事件记录:记录点击的ip以及点击时间
    根据上述步骤,抽象出TransformFilter过滤器,并分别提供了上述四个步骤中的过滤器。
package com.cube.dp.cor.filter;

import com.cube.dp.cor.context.TransformContext;
import org.springframework.core.Ordered;

/**
 * @author cube.li
 * @date 2021/12/13 20:56
 * <p>
 * 短链转换过滤器
 */
public interface TransformFilter extends Ordered {

    /**
     * 执行过滤逻辑
     *
     * @param context 上下文
     */
    void doFilter(TransformContext context);

    /**
     * 初始化钩子方法
     *
     * @param context context
     */
    default void init(TransformContext context) {
    }

    /**
     * 获取拦截器的顺序
     *
     * @return 顺序值
     */
    @Override
    int getOrder();
}

通过getOrder方法决定该过滤器的执行顺序,值越小优先级越高;init方法用于过滤器创建时初始化一部分信息,默认提供了空实现,如果有特定需求可以重写该方法。

package com.cube.dp.cor.filter;

import com.cube.dp.base.error.CommonApiResultCode;
import com.cube.dp.base.error.CommonBuException;
import com.cube.dp.cor.context.TransformContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.HashSet;
import java.util.Set;

/**
 * @author cube.li
 * @date 2021/12/13 21:34
 * <p>
 * 短链转换黑名单过滤器
 */
@Component
@Slf4j
public class BlackIpTransformFilter implements TransformFilter {

    /**
     * ip黑名单,这里固定写死
     */
    private static final Set<String> IP_SET = new HashSet<>();

    static {
        IP_SET.add("113.96.233.143");
    }

    @Override
    public void doFilter(TransformContext context) {
        log.debug("ip黑名单过滤...");
        if (IP_SET.contains(context.getIp())) {
            throw new CommonBuException(CommonApiResultCode.BLACK_IP);
        }
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

黑名单过滤器,如果请求ip在黑名单中则拒绝处理。

package com.cube.dp.cor.filter;

import com.cube.dp.cor.common.TransformErrorCode;
import com.cube.dp.cor.common.TransformException;
import com.cube.dp.cor.context.TransformContext;
import com.cube.dp.cor.context.TransformStatus;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
 * @author cube.li
 * @date 2021/12/13 21:37
 * <p>
 * 短链映射过滤器,将短链转换为原始长链
 */
@Component
@Slf4j
public class UrlMappingTransformFilter implements TransformFilter {

    /**
     * 短链与原始链接的映射,实际项目中应该从数据库中查找
     */
    private static final Map<String, String> SHORT_URL_MAP = new HashMap<>();

    static {
        SHORT_URL_MAP.put("1deN4", "https://www.jianshu.com/u/94fe913745b7");
        SHORT_URL_MAP.put("jji3N", "https://www.zhihu.com/question/315448681");
    }

    @Override
    public void doFilter(TransformContext context) {
        log.debug("转换转换过滤器...");
        if (SHORT_URL_MAP.containsKey(context.getShortUrlCode())) {
            //找到对应的长链
            context.setOriginLongUrl(getOriginUrl(context.getShortUrlCode()));
            context.setTransformStatus(TransformStatus.TRANSFORM_SUCCESS);
        } else {
            context.setTransformStatus(TransformStatus.TRANSFORM_FAIL);
            throw new TransformException(TransformErrorCode.INVALID_ST);
        }
    }

    @Override
    public int getOrder() {
        return 1;
    }

    /**
     * 获取原始链接
     *
     * @param shortUrl 短链
     * @return 原始链接
     */
    @NonNull
    private String getOriginUrl(String shortUrl) {
        return SHORT_URL_MAP.get(shortUrl);
    }
}

短链映射过滤器,根据短链找到对应的长链

package com.cube.dp.cor.filter;

import com.cube.dp.base.utils.ResponseUtils;
import com.cube.dp.cor.context.TransformContext;
import com.cube.dp.cor.context.TransformStatus;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

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

/**
 * @author cube.li
 * @date 2021/12/13 21:44
 * <p>
 * 短信转换重定向过滤器,在短链转长链完成后进行重定向
 */
@Component
@Slf4j
public class RedirectTransformFilter implements TransformFilter {

    @Override
    public void doFilter(TransformContext context) {
        log.debug("短链转换-重定向...");
        try {
            HttpServletResponse response = ResponseUtils.currentResponse();
            //noinspection ConstantConditions
            redirect302(response, context.getOriginLongUrl());
            context.setTransformStatus(TransformStatus.REDIRECTION_SUCCESS);
        } catch (Exception e) {
            context.setTransformStatus(TransformStatus.REDIRECTION_FAIL);
        }

    }

    /**
     * 永久重定向,由于浏览器重定向缓存无法记录到所有的短链转换行为
     *
     * @param response      响应
     * @param originLongUrl 原始链接
     */
    private void redirect301(HttpServletResponse response, String originLongUrl) {
        response.setStatus(301);
        response.setHeader("Location", originLongUrl);
    }

    /**
     * 临时重定向,浏览器不会缓存重定向信息会记录到每次点击
     *
     * @param response      响应
     * @param originLongUrl 原始链接
     */
    private void redirect302(HttpServletResponse response, String originLongUrl) throws IOException {
        response.setStatus(302);
        response.sendRedirect(originLongUrl);
    }

    @Override
    public int getOrder() {
        return 2;
    }
}

短信转换重定向过滤器,在短链转长链完成后进行重定向

package com.cube.dp.cor.filter;

import com.cube.dp.cor.context.TransformContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * @author cube.li
 * @date 2021/12/13 21:47
 * <p>
 * 记录短链转换过滤器,用于记录每一次的短链点击事件
 */
@Component
@Slf4j
public class RecordTransformFilter implements TransformFilter {

    @Override
    public void doFilter(TransformContext context) {
        log.debug("记录短链点击行为...");
    }

    @Override
    public int getOrder() {
        return 10;
    }
}

记录短链转换过滤器,用于记录每一次的短链点击事件

package com.cube.dp.cor.filter;

/**
 * @author cube.li
 * @date 2021/12/13 21:18
 * <p>
 * 短链转换过滤器链
 */
public interface TransformFilterChain {

    /**
     * 执行过滤
     */
    void doFilter();


}

过滤器链接口

package com.cube.dp.cor.filter;

import com.cube.dp.cor.context.TransformContext;
import lombok.extern.slf4j.Slf4j;

import java.util.LinkedList;
import java.util.List;

/**
 * @author cube.li
 * @date 2021/12/13 21:50
 * <p>
 * 默认实现的短链转换过滤器链
 */
@Slf4j
public class DefaultTransformFilterChain implements TransformFilterChain {

    private List<TransformFilter> filters = new LinkedList<>();

    private TransformContext context;

    public DefaultTransformFilterChain(TransformContext context) {
        this.context = context;
    }


    @Override
    public void doFilter() {
        filters.forEach(filter -> filter.doFilter(context));
    }

    /**
     * 添加过滤器,并将其加入合适的位置(根据order升序排序)
     *
     * @param filter 待添加的过滤器
     */
    public void addTransformFilter(TransformFilter filter) {
        //根据order添加到合适的位置
    }

    /**
     * 添加过滤器
     *
     * @param sortedFilters 必须是排过序的集合
     */
    public void addTransformFilters(List<TransformFilter> sortedFilters) {
        this.filters = sortedFilters;
    }
}

默认的过滤器链实现类,该类中持有多个过滤器且根据getOrder维护了它们之间的处理顺序

package com.cube.dp.cor.filter;

import com.cube.dp.cor.context.TransformContext;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.stereotype.Component;

import java.util.Comparator;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author cube.li
 * @date 2021/12/13 21:53
 * <p>
 * 短链转换过滤器链工厂
 */
@Component
public class TransformFilterChainFactory implements BeanFactoryAware {

    private ListableBeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = (ListableBeanFactory) beanFactory;
    }

    /**
     * 根据短链转换上下文创建短链转换过滤器链
     *
     * @param context 上下文
     * @return 过滤器链
     */
    public TransformFilterChain defaultFilterChain(TransformContext context) {
        //从容器中获取所有的短链转换过滤器
        Map<String, TransformFilter> filterMap = beanFactory.getBeansOfType(TransformFilter.class);

        DefaultTransformFilterChain chain = new DefaultTransformFilterChain(context);
        //根据order按照升序排序
        chain.addTransformFilters(filterMap.values()
                .stream()
                .sorted(Comparator.comparingInt(TransformFilter::getOrder))
                .collect(Collectors.toList()));
        return chain;
    }
}

过滤器链创建工厂,通过该工厂创建过滤器对象

package com.cube.dp.cor.controller;

import com.cube.dp.base.response.ApiResult;
import com.cube.dp.cor.context.TransformContext;
import com.cube.dp.cor.filter.TransformFilterChain;
import com.cube.dp.cor.filter.TransformFilterChainFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

/**
 * @author cube.li
 * @date 2021/12/23 15:36
 */
@RestController
@Slf4j
public class TransformController {

    @Autowired
    private TransformFilterChainFactory chainFactory;

    @GetMapping("st/{code}")
    public ApiResult<Void> transform(@PathVariable String code, HttpServletRequest request) {
        TransformContext context = TransformContext.of(request, code);
        TransformFilterChain chain = chainFactory.defaultFilterChain(context);
        chain.doFilter();
        return ApiResult.success();
    }
}

短链点击Controller
在浏览器中访问http://localhost:8080/st/1deN4,页面重定向到指定页面,控制台输出如下:

2021-12-23 20:02:55.783  INFO 23540 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2021-12-23 20:02:55.784  INFO 23540 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2021-12-23 20:02:55.806 DEBUG 23540 --- [nio-8080-exec-3] c.c.d.cor.filter.BlackIpTransformFilter  : ip黑名单过滤...
2021-12-23 20:02:55.806 DEBUG 23540 --- [nio-8080-exec-3] c.c.d.c.f.UrlMappingTransformFilter      : 转换转换过滤器...
2021-12-23 20:02:55.806 DEBUG 23540 --- [nio-8080-exec-3] c.c.d.c.filter.RedirectTransformFilter   : 短链转换-重定向...
2021-12-23 20:02:55.807 DEBUG 23540 --- [nio-8080-exec-3] c.c.dp.cor.filter.RecordTransformFilter  : 记录短链点击行为...

在实际项目中,为了更好的用户体验,应该把抛出异常中断处理改成转发到特定的错误页面,而不是直接展示出错误信息。

总结

不想写总结了,实现代码请参考:示例代码

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

推荐阅读更多精彩内容