简介
9月份做了个短信发送的功能,考虑到短信文本字数的限制,需要将原始长链接转换为短链发送,并且需要记录每次的短链点击行为。点击短链之后的处理逻辑主要为:ip黑名单过滤 ->> 短链转换成原始链接 ->>重定向到原始链接->>点击行为记录;就是对同一个请求按照步骤进行链式处理,所以很自然的就想到了要通过责任链模式实现。
责任链模式
定义:为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象持有下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到处理完成为止。
特点
- 降低了对象之间的耦合度,该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送着和接收者之间无须拥有对方的明确信息。
- 增强了系统的可拓展性,可以根据需要增加新的请求处理类,满足开闭原则。
- 增强了为对象指派职责的灵活性,当工作流程发生变化,可以动态地改变链内的成员或者调动它们之间的次序,也可新增或者删除责任。
- 责任链简化了对象之间的连接,每个对象只需保持一个指向其后继者的引用,不需要保持其他所有处理者的引用。
- 责任分担:每个处理类只需要关注自己的责任,明确各类的责任范围,符合类的单一职责原则。
常见模式
模式一:
在这种模式下,请求上下文沿着处理器传递,每个处理器都有处理请求的机会,直至全部的处理器处理完成后请求结束。
模式二:
链式处理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 : 记录短链点击行为...
在实际项目中,为了更好的用户体验,应该把抛出异常中断处理改成转发到特定的错误页面,而不是直接展示出错误信息。
总结
不想写总结了,实现代码请参考:示例代码
网友评论