# 扩展
# 概述
小皮同学接到了一个开发任务:开发一个基于 zookeeper 的配置中心。熟悉 zookeeper 的 小皮信心满满,两三天就将这个功能开发完毕,单测、文档均已搞定,于是汇报给了 leader。
leader 很是满意,不过前方运维传来了不幸的消息,zookeeper 集群出现重大问题,不能 再分配相应机器供配置调用。没有办法,只能安排小皮使用公司的 etcd 集群重新开发配置 中心了。
小皮很是难过,只能硬着头皮进行魔改。重构的时候,小皮发现原来的代码严重耦合,虽然业务完成了, 但不易维护和添加新特性。小皮苦思良久,又花了一个星期的时间将配置中心完全重构, 现在配置中心不仅支持 etcd 和 zookeeper,而且还能一键切换,leader 很是高兴,奖励了小皮。
对于一个业务接口,不同的具体实现,支持一键切换,这样的开发模式是解决复杂业务,保 持系统稳定的一个重要解决方案。
lin-cms 将这样的功能点单独抽象出了一个概念——扩展
。扩展应具有以下几种特点:
- 一种功能,多种实现。
- 独立于项目,不依赖项目的业务。
- 实现替换简单,开箱即用。
# 实例
我们还是以file
作为例子来详细的讲解扩展的使用,文件扩展的代码目录结构如下:
├── File.java
├── FileConstant.java
├── FileProperties.java
├── FileUtil.java
├── LocalUploader.java
├── PreHandler.java
├── Uploader.java
└── config.properties
首先,你可以将扩展理解为一个业务点,既然是业务点,那么一定会对外提供业务支持;没错,
file
对外提供Uploader
的业务支持,即文件上传的业务支持。
Uploader
是一个接口,同时它也是一个出口,一个提供文件上传服务的出口。而其它文件
都是服务于Uploader
的,如File
是上传文件信息的数据容器,FileUtil
则是基础的工
具类。
既然Uploader
是一个服务接口,那么它必然有服务使用者,目前这个服务的使用者是业务
层中的FileService
。
好了,我们回到正题,继续来谈代码结构;FileConstant 是字符串常量相关类,方便字符串
的维护;FileProperties 是文件上传的相关配置类,它与config.properties
搭配,一个
提供配置文件,一个提供程序配置类;LocalUploader 是Uploader
的具体实现,它是第一
个默认实现,你也可以提供相关的实现来满足你自己的业务。PreHandler 非必须,只
是Uploader
业务实现的一个点。
谈到这里,我们来总结一下扩展:
- 扩展其实就是一个微型接口,它有自己独立的配置,独立的逻辑和实现。
- 扩展必须独立,它不应该依赖工程,而应该让工程依赖于它。
- 扩展必须提供一个接口作为出口,该接口可以有多个实现。
# 实践
说了那么多,你或许还是云里雾里,下面我们将开发一个基础限流扩展
来实操一下。
首先,我们在src/main/java/io/github/talelin/latticy/extension
扩展目录下新建一个
目录limit
。
并在src/main/java/io/github/talelin/latticy/extension/limit
目录下新建一
个Limiter.java
接口:
package io.github.talelin.latticy.extension.limit;
import javax.servlet.http.HttpServletRequest;
public interface Limiter {
boolean handle(HttpServletRequest request);
}
Limiter
接口只有一个需要实现方法,方法拿到请求request
并根据请求数据来判断请求
是否过多。
limit
扩展需要配置来管理具体的 QPS,即一个 IP 每秒至多请求多少次,配置如下:
lin.cms.limit.value=5
配置文件十分简单,仅一个配置,用来配置 QPS 值,同时我们也为该配置新建相应的配置 类:
package io.github.talelin.latticy.extension.limit;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "lin.cms.limit")
@PropertySource(value = "classpath:io/github/talelin/latticy/extension/limit/config.properties", encoding = "UTF-8")
public class LimitProperties {
private Integer value = 5;
public Integer getValue() {
return value;
}
public void setValue(Integer value) {
this.value = value;
}
}
有了配置和接口以后,我们需要一个限流具体实现,考虑到基本限流,无需太复杂,我们使
用guava
的 RateLimit 来实现。
在pom.xml
文件中添加上 guava 的依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.0-jre</version>
</dependency>
再来具体实现一个限流组件MemoryLimiter
:
package io.github.talelin.latticy.extension.limit;
import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
@Slf4j
public class MemoryLimiter implements Limiter {
private Map<String, RateLimiter> record = new ConcurrentHashMap<>();
@Value("${lin.cms.limit.value:5}")
private Integer value;
@Override
public boolean handle(HttpServletRequest request) {
String uniqueId = getUniqueId(request);
log.info("uniqueId: {}", uniqueId);
RateLimiter currentLimiter = record.get(uniqueId);
if (currentLimiter != null) {
return currentLimiter.tryAcquire(1);
} else {
// 减去当前访问的一次
RateLimiter limiter = RateLimiter.create(value);
record.put(uniqueId, limiter);
return true;
}
}
private String getUniqueId(HttpServletRequest request) {
return request.getLocalAddr();
}
}
有了限流组件后,我们还需要将组件实施到应用上,我们在spring mvc
拦截器上来实现限
流,如果某个 ip 在某一秒内请求太多,则拒绝请求。
因此我们在limit
目录下新建一个拦截器LimitInterceptor
:
package io.github.talelin.latticy.extension.limit;
import io.github.talelin.autoconfigure.exception.RequestLimitException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
@Slf4j
public class LimitInterceptor extends HandlerInterceptorAdapter {
@Autowired
private Limiter limiter;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
boolean ok = limiter.handle(request);
log.info("limit val : {}", ok);
if (!ok) {
throw new RequestLimitException();
}
return super.preHandle(request, response, handler);
}
}
拦截器的逻辑很简单,如果limiter
判断当前请求 ip 请求太多则拒绝,并给出异常,否
则进入后面的流程。
我们再将拦截器挂载到 web 应用中,
在src/main/java/io/github/talelin/latticy/common/configure/WebConfig.java
文件中
,我们修改一下代码:
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LimitInterceptor limitInterceptor; // 1
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(limitInterceptor); // 2
if (authEnabled) {
//开发环境忽略签名认证
registry.addInterceptor(authorizeInterceptor)
.excludePathPatterns(getDirServePath());
}
if (requestLogEnabled) {
registry.addInterceptor(requestLogInterceptor);
}
registry.addInterceptor(logInterceptor);
}
}
添加上面两个地方的代码,我们的限流扩展就可以正常工作了。完成后的代码目录如下:
src/main/java/io/github/talelin/latticy/extension/limit
├── LimitInterceptor.java
├── LimitProperties.java
├── Limiter.java
├── MemoryLimiter.java
└── config.properties
运行程序,如果某个 ip 的请求过多,则会报TOO_MANY_REQUESTS
的异常。