文章目录
前言
本篇会用外观模式来设计一个处理白名单业务的中间件,将外观模式实际用到项目中。
注意:设计模式学的是设计思想,而不是固定的实现方式。
外观模式介绍
定义:
外观模式又叫作门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。
外观模式是“迪米特法则”的典型应用,它有以下主要优点:
- 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类;
- 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易;
- 降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象。
通过以上介绍可以发现第三方SDK、开源类库,基本上都使用了外观模式。
违背设计模式实现
伪代码如下:
public static boolean whiteList(Sting userId){
//白名单列表
List<String> list =new ArrayList();
list.add("1001");
list.add("1002");
list.add("1003");
//校验是在白名单中
if(!list.contains(userId)){
return false;
}
return true;
}
写上一个静态方法简单实现白名单功能,在controller层的接口都调用这个接口,如果是一个服务勉强还能接受,如果涉及是多个服务则需要将这个方法copy到其他相关服务中了,如果这块的业务需要变更涉及的服务都改,这种设计方式耦合度太高了,代码也冗余不够优雅。
设计模式重构
1.AOP切面注解
pom(示例):
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>facade</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<name>facade</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
</dependencies>
2.配置服务类
代码如下(示例):
/**
* 配置服务类
* @author winter
*/
public class StarterService {
private String userStr;
public StarterService(String userStr) {
this.userStr = userStr;
}
public String[] split(String separatorChar) {
return StringUtils.split(this.userStr, separatorChar);
}
}
为了获取springboot中配置文件的信息内容。
3.配置类注解定义
代码如下(示例):
/**
* 配置类注解定义
* @author winter
*/
@ConfigurationProperties("white.list")
public class StarterServiceProperties {
private String userStr;
public String getUserStr() {
return userStr;
}
public void setUserStr(String userStr) {
this.userStr = userStr;
}
}
获取到 application.yml 中添加 white.list的配置信息。
4.获取自定义配置类信息
/**
* 获取自定义配置类信息
* @author winter
*/
@Configuration
@ConditionalOnClass(StarterService.class)
@EnableConfigurationProperties(StarterServiceProperties.class)
public class StarterAutoConfigure {
@Autowired
private StarterServiceProperties properties;
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "white.list", value = "enabled", havingValue = "true")
StarterService starterService() {
return new StarterService(properties.getUserStr());
}
}
主要是对注解@Configuration、@ConditionalOnClass、@EnableConfigurationProperties的定义,直接读取配置文件中数据并将StarterService加载到bean中。
5.切面注解定义
/**
* 注解定义
* @author winter
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DoDoor {
String key() default "";
}
切面注解定义了外观模式切面注解。
6.白名单切面逻辑
/**
* 切面定义
* @author winter
*/
@Aspect
@Component
public class DoJoinPoint {
private Logger logger = LoggerFactory.getLogger(DoJoinPoint.class);
@Autowired
private StarterService starterService;
@Pointcut("@annotation(com.example.facade.annotation.DoDoor)")
public void aopPoint() {
}
@Around("aopPoint()")
public Object doRouter(ProceedingJoinPoint jp) throws Throwable {
//获取内容
Method method = getMethod(jp);
DoDoor door = method.getAnnotation(DoDoor.class);
//获取字段值
String keyValue = getFiledValue(door.key(), jp.getArgs());
logger.info("handler method:{} value:{}", method.getName(), keyValue);
if (null == keyValue || "".equals(keyValue)) {
return jp.proceed();
}
//配置内容
String[] split = starterService.split(",");
//白名单过滤
for (String str : split) {
if (keyValue.equals(str)) {
return jp.proceed();
}
}
//拦截
return returnObject(method);
}
private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
Signature sig = jp.getSignature();
MethodSignature methodSignature = (MethodSignature) sig;
return getClass(jp).getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
}
private Class<? extends Object> getClass(JoinPoint jp) throws NoSuchMethodException {
return jp.getTarget().getClass();
}
//返回对象
private Object returnObject(Method method) throws IllegalAccessException, InstantiationException {
Class<?> returnType = method.getReturnType();
return JSON.parseObject("用户id不在白名单中", returnType);
}
//获取属性值
private String getFiledValue(String filed, Object[] args) {
String filedValue = null;
for (Object arg : args) {
try {
if (null == filedValue || "".equals(filedValue)) {
filedValue = BeanUtils.getProperty(arg, filed);
} else {
break;
}
} catch (Exception e) {
if (args.length == 1) {
return args[0].toString();
}
}
}
return filedValue;
}
}
主要是用切面方式实现白名单校验的功能。
7.引入中间件POM
<dependency>
<groupId>com.example</groupId>
<artifactId>facade</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
备注:一般都是直接上传到maven上。
8.配置application.yml
white:
list:
enabled: true
user-str: 1001,1002,1003
9.在controller中添加自定义注解
@GetMapping("/user")
@DoDoor(key = "userId")
public String getUser(@RequestParam String userId){
return "获取到用户信息";
}
总结
通过中间件的方式实现外观模式,这种设计可以很好的增强代码的隔离性及复用性,不仅使用非常灵活,也降低了对每一个系统开发白名单拦截服务带来的风险及测试成本。