作者:小傅哥
博客:https://bugstack.cn
沉淀、分享、成長,讓自己和他人都能有所收獲!😄
一、前言
你感受到的容易,一定有人為你承擔不容易
這句話更像是描述生活的,許許多多的磕磕絆絆總有人為你提供躲雨的屋檐和避風的港灣。其實編程開發的團隊中也一樣有人只負責CRUD中的簡單調用,去使用團隊中高級程序員開發出來的核心服務和接口。這樣的編程開發對於初期剛進入程序員行業的小伙伴來說鍛煉鍛煉還是不錯的,但隨着開發的日子越來越久一直做這樣的事情就很難得到成長,也想努力的去做一些更有難度的承擔,以此來增強個人的技術能力。
沒有最好的編程語言,語言只是工具
刀槍棍棒、斧鉞鈎叉、包子油條、盒子麻花,是語言。五郎八卦棍、十二路彈腿、洪家鐵線拳,是設計。記得葉問里有一句台詞是:金山找:今天我北方拳術,輸給你南方拳術了。葉問:你錯了,不是南北拳的問題,是你的問題。
所以當你編程開發寫的久了,就不會再特別在意用的語言,而是為目標服務,用最好的設計能力也就是編程的智慧做出做最完美的服務。這也就是編程人員的價值所在!
設計與反設計以及過渡設計
設計模式是解決程序中不合理、不易於擴展、不易於維護的問題,也是干掉大部分ifelse
的利器,在我們常用的框架中基本都會用到大量的設計模式來構建組件,這樣也能方便框架的升級和功能的擴展。但!如果不能合理的設計以及亂用設計模式,會導致整個編程變得更加復雜難維護,也就是我們常說的;反設計
、過渡設計
。而這部分設計能力也是從實踐的項目中獲取的經驗,不斷的改造優化摸索出的最合理的方式,應對當前的服務體量。
二、開發環境
- JDK 1.8
- Idea + Maven
- SpringBoot 2.1.2.RELEASE
- 涉及工程三個,可以通過關注公眾號:
bugstack蟲洞棧
,回復源碼下載
獲取(打開獲取的鏈接,找到序號18)
工程 | 描述 |
---|---|
itstack-demo-design-10-00 | 場景模擬工程;模擬一個提供接口服務的SpringBoot工程 |
itstack-demo-design-10-01 | 使用一坨代碼實現業務需求 |
itstack-demo-design-10-02 | 通過設計模式開發為中間件,包裝通用型核心邏輯 |
三、外觀模式介紹
外觀模式也叫門面模式,主要解決的是降低調用方的使用接口的復雜邏輯組合。這樣調用方與實際的接口提供方提供方提供了一個中間層,用於包裝邏輯提供API接口。有些時候外觀模式也被用在中間件層,對服務中的通用性復雜邏輯進行中間件層包裝,讓使用方可以只關心業務開發。
那么這樣的模式在我們的所見產品功能中也經常遇到,就像幾年前我們注冊一個網站時候往往要添加很多信息,包括;姓名、昵稱、手機號、QQ、郵箱、住址、單身等等,但現在注冊成為一個網站的用戶只需要一步即可,無論是手機號還是微信也都提供了這樣的登錄服務。而對於服務端應用開發來說以前是提供了一個整套的接口,現在注冊的時候並沒有這些信息,那么服務端就需要進行接口包裝,在前端調用注冊的時候服務端獲取相應的用戶信息(從各個渠道),如果獲取不到會讓用戶后續進行補全(營銷補全信息給獎勵),以此來拉動用戶的注冊量和活躍度。
四、案例場景模擬
在本案例中我們模擬一個將所有服務接口添加白名單的場景
在項目不斷壯大發展的路上,每一次發版上線都需要進行測試,而這部分測試驗證一般會進行白名單開量或者切量的方式進行驗證。那么如果在每一個接口中都添加這樣的邏輯,就會非常麻煩且不易維護。另外這是一類具備通用邏輯的共性需求,非常適合開發成組件,以此來治理服務,讓研發人員更多的關心業務功能開發。
一般情況下對於外觀模式的使用通常是用在復雜或多個接口進行包裝統一對外提供服務上,此種使用方式也相對簡單在我們平常的業務開發中也是最常用的。你可能經常聽到把這兩個接口包裝一下,但在本例子中我們把這種設計思路放到中間件層,讓服務變得可以統一控制。
1. 場景模擬工程
itstack-demo-design-10-00
└── src
├── main
│ ├── java
│ │ └── org.itstack.demo.design
│ │ ├── domain
│ │ │ └── UserInfo.java
│ │ ├── web
│ │ │ └── HelloWorldController.java
│ │ └── HelloWorldApplication.java
│ └── resources
│ └── application.yml
└── test
└── java
└── org.itstack.demo.test
└── ApiTest.java
- 這是一個
SpringBoot
的HelloWorld
工程,在工程中提供了查詢用戶信息的接口HelloWorldController.queryUserInfo
,為后續擴展此接口的白名單過濾做准備。
2. 場景簡述
2.1 定義基礎查詢接口
@RestController
public class HelloWorldController {
@Value("${server.port}")
private int port;
/**
* key:需要從入參取值的屬性字段,如果是對象則從對象中取值,如果是單個值則直接使用
* returnJson:預設攔截時返回值,是返回對象的Json
*
* http://localhost:8080/api/queryUserInfo?userId=1001
* http://localhost:8080/api/queryUserInfo?userId=小團團
*/
@RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
public UserInfo queryUserInfo(@RequestParam String userId) {
return new UserInfo("蟲蟲:" + userId, 19, "天津市南開區旮旯胡同100號");
}
}
- 這里提供了一個基本的查詢服務,通過入參
userId
,查詢用戶信息。后續就需要在這里擴展白名單,只有指定用戶才可以查詢,其他用戶不能查詢。
2.2 設置Application啟動類
@SpringBootApplication
@Configuration
public class HelloWorldApplication {
public static void main(String[] args) {
SpringApplication.run(HelloWorldApplication.class, args);
}
}
- 這里是通用的
SpringBoot
啟動類。需要添加的是一個配置注解@Configuration
,為了后續可以讀取白名單配置。
五、用一坨坨代碼實現
一般對於此種場景最簡單的做法就是直接修改代碼
累加if
塊幾乎是實現需求最快也是最慢的方式,快是修改當前內容很快,慢是如果同類的內容幾百個也都需要如此修改擴展和維護會越來越慢。
1. 工程結構
itstack-demo-design-10-01
└── src
└── main
└── java
└── org.itstack.demo.design
└── HelloWorldController.java
- 以上的實現是模擬一個Api接口類,在里面添加白名單功能,但類似此類的接口會有很多都需要修改,所以這也是不推薦使用此種方式的重要原因。
2. 代碼實現
public class HelloWorldController {
public UserInfo queryUserInfo(@RequestParam String userId) {
// 做白名單攔截
List<String> userList = new ArrayList<String>();
userList.add("1001");
userList.add("aaaa");
userList.add("ccc");
if (!userList.contains(userId)) {
return new UserInfo("1111", "非白名單可訪問用戶攔截!");
}
return new UserInfo("蟲蟲:" + userId, 19, "天津市南開區旮旯胡同100號");
}
}
- 在這里白名單的代碼占據了一大塊,但它又不是業務中的邏輯,而是因為我們上線過程中需要做的開量前測試驗證。
- 如果你日常對待此類需求經常是這樣開發,那么可以按照此設計模式進行優化你的處理方式,讓后續的擴展和摘除更加容易。
六、外觀模式重構代碼
接下來使用外觀器模式來進行代碼優化,也算是一次很小的重構。
這次重構的核心是使用外觀模式也可以說門面模式,結合SpringBoot
中的自定義starter
中間件開發的方式,統一處理所有需要白名單的地方。
后續接下來的實現中,會涉及的知識;
- SpringBoot的starter中間件開發方式。
- 面向切面編程和自定義注解的使用。
- 外部自定義配置信息的透傳,SpringBoot與Spring不同,對於此類方式獲取白名單配置存在差異。
1. 工程結構
itstack-demo-design-10-02
└── src
├── main
│ ├── java
│ │ └── org.itstack.demo.design.door
│ │ ├── annotation
│ │ │ └── DoDoor.java
│ │ ├── config
│ │ │ ├── StarterAutoConfigure.java
│ │ │ ├── StarterService.java
│ │ │ └── StarterServiceProperties.java
│ │ └── DoJoinPoint.java
│ └── resources
│ └── META_INF
│ └── spring.factories
└── test
└── java
└── org.itstack.demo.test
└── ApiTest.java
門面模式模型結構
- 以上是外觀模式的中間件實現思路,右側是為了獲取配置文件,左側是對於切面的處理。
- 門面模式可以是對接口的包裝提供出接口服務,也可以是對邏輯的包裝通過自定義注解對接口提供服務能力。
2. 代碼實現
2.1 配置服務類
public class StarterService {
private String userStr;
public StarterService(String userStr) {
this.userStr = userStr;
}
public String[] split(String separatorChar) {
return StringUtils.split(this.userStr, separatorChar);
}
}
- 以上類的內容較簡單只是為了獲取配置信息。
2.2 配置類注解定義
@ConfigurationProperties("itstack.door")
public class StarterServiceProperties {
private String userStr;
public String getUserStr() {
return userStr;
}
public void setUserStr(String userStr) {
this.userStr = userStr;
}
}
- 用於定義好后續在
application.yml
中添加itstack.door
的配置信息。
2.3 自定義配置類信息獲取
@Configuration
@ConditionalOnClass(StarterService.class)
@EnableConfigurationProperties(StarterServiceProperties.class)
public class StarterAutoConfigure {
@Autowired
private StarterServiceProperties properties;
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "itstack.door", value = "enabled", havingValue = "true")
StarterService starterService() {
return new StarterService(properties.getUserStr());
}
}
- 以上代碼是對配置的獲取操作,主要是對注解的定義;
@Configuration
、@ConditionalOnClass
、@EnableConfigurationProperties
,這一部分主要是與SpringBoot的結合使用。
2.4 切面注解定義
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DoDoor {
String key() default "";
String returnJson() default "";
}
- 定義了外觀模式門面注解,后續就是此注解添加到需要擴展白名單的方法上。
- 這里提供了兩個入參,key:獲取某個字段例如用戶ID、returnJson:確定白名單攔截后返回的具體內容。
2.5 白名單切面邏輯
@Aspect
@Component
public class DoJoinPoint {
private Logger logger = LoggerFactory.getLogger(DoJoinPoint.class);
@Autowired
private StarterService starterService;
@Pointcut("@annotation(org.itstack.demo.design.door.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("itstack door 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(door, 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(DoDoor doGate, Method method) throws IllegalAccessException, InstantiationException {
Class<?> returnType = method.getReturnType();
String returnJson = doGate.returnJson();
if ("".equals(returnJson)) {
return returnType.newInstance();
}
return JSON.parseObject(returnJson, 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;
}
}
- 這里包括的內容較多,核心邏輯主要是;
Object doRouter(ProceedingJoinPoint jp)
,接下來我們分別介紹。
@Pointcut("@annotation(org.itstack.demo.design.door.annotation.DoDoor)")
定義切面,這里采用的是注解路徑,也就是所有的加入這個注解的方法都會被切面進行管理。
getFiledValue
獲取指定key也就是獲取入參中的某個屬性,這里主要是獲取用戶ID,通過ID進行攔截校驗。
returnObject
返回攔截后的轉換對象,也就是說當非白名單用戶訪問時則返回一些提示信息。
doRouter
切面核心邏輯,這一部分主要是判斷當前訪問的用戶ID是否白名單用戶,如果是則放行jp.proceed();
,否則返回自定義的攔截提示信息。
3. 測試驗證
這里的測試我們會在工程:itstack-demo-design-10-00
中進行操作,通過引入jar包,配置注解的方式進行驗證。
3.1 引入中間件POM配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>itstack-demo-design-10-02</artifactId>
</dependency>
- 打包中間件工程,給外部提供jar包服務
3.2 配置application.yml
# 自定義中間件配置
itstack:
door:
enabled: true
userStr: 1001,aaaa,ccc #白名單用戶ID,多個逗號隔開
- 這里主要是加入了白名單的開關和白名單的用戶ID,逗號隔開。
3.3 在Controller中添加自定義注解
/**
* http://localhost:8080/api/queryUserInfo?userId=1001
* http://localhost:8080/api/queryUserInfo?userId=小團團
*/
@DoDoor(key = "userId", returnJson = "{\"code\":\"1111\",\"info\":\"非白名單可訪問用戶攔截!\"}")
@RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
public UserInfo queryUserInfo(@RequestParam String userId) {
return new UserInfo("蟲蟲:" + userId, 19, "天津市南開區旮旯胡同100號");
}
- 這里核心的內容主要是自定義的注解的添加
@DoDoor
,也就是我們的外觀模式中間件化實現。 - key:需要從入參取值的屬性字段,如果是對象則從對象中取值,如果是單個值則直接使用。
- returnJson:預設攔截時返回值,是返回對象的Json。
3.4 啟動SpringBoot
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.2.RELEASE)
2020-06-11 23:56:55.451 WARN 65228 --- [ main] ion$DefaultTemplateResolverConfiguration : Cannot find template location: classpath:/templates/ (please add some templates or check your Thymeleaf configuration)
2020-06-11 23:56:55.531 INFO 65228 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2020-06-11 23:56:55.533 INFO 65228 --- [ main] o.i.demo.design.HelloWorldApplication : Started HelloWorldApplication in 1.688 seconds (JVM running for 2.934)
- 啟動正常,SpringBoot已經啟動可以對外提供服務。
3.5 訪問接口接口測試
白名單用戶訪問
http://localhost:8080/api/queryUserInfo?userId=1001
{"code":"0000","info":"success","name":"蟲蟲:1001","age":19,"address":"天津市南開區旮旯胡同100號"}
- 此時的測試結果正常,可以拿到接口數據。
非白名單用戶訪問
http://localhost:8080/api/queryUserInfo?userId=小團團
{"code":"1111","info":"非白名單可訪問用戶攔截!","name":null,"age":null,"address":null}
- 這次我們把
userId
換成小團團
,此時返回的信息已經是被攔截的信息。而這個攔截信息正式我們自定義注解中的信息:@DoDoor(key = "userId", returnJson = "{\"code\":\"1111\",\"info\":\"非白名單可訪問用戶攔截!\"}")
七、總結
- 以上我們通過中間件的方式實現外觀模式,這樣的設計可以很好的增強代碼的隔離性,以及復用性,不僅使用上非常靈活也降低了每一個系統都開發這樣的服務帶來的風險。
- 可能目前你看這只是非常簡單的白名單控制,是否需要這樣的處理。但往往一個小小的開始會影響着后續無限的擴展,實際的業務開發往往也要復雜的很多,不可能如此簡單。因而使用設計模式來讓代碼結構更加干凈整潔。
- 很多時候不是設計模式沒有用,而是自己編程開發經驗不足導致即使學了設計模式也很難駕馭。畢竟這些知識都是經過一些實際操作提煉出來的精華,但如果你可以按照本系列文章中的案例方式進行學習實操,還是可以增強這部分設計能力的。