前言
對於Spring的IoC(控制反轉) 、DI(依賴注入)這兩個概念,使用過spring的都很熟悉,使用容器來控制相關的對象的生命週期和對象間的關係。擴散下思維,可以使用一個容器來存儲相關的http的controller類,根據http的請求參數來遍歷這個容器執行哪個類哪個方法。但是如何是http調用這個容器呢?spring還提供了事件監聽(事件監聽概念這裏不做說明)的實現,實現一個監聽,利用spring-mvc的手動註冊控制器來監聽相關的http請求,調用容器執行相關的類和方法。
目標
先看下spring web的普通的http請求
@RestController
public class HttpController {
@GetMapping("/http/welcome")
@ResponseBody
public string getLeagueById() {
return "Hello word!";
}
}
而目標需要改造成這樣
@MessageController
public class HttpController {
@MessageMapping(messageType = PBMessageType.HTTP_WELCOME_VALUE)
public PBString adminLogin(PBWelcome req) {
return PBWelcome.newBuilder().setValue("Hello word!").build();
}
}
其實可以看出兩者是基本相同的,但下面這個自定義了MessageController和MessageMapping使其支持Protocol Buffer(PB)格式。爲什麼要使用PB數據呢?因爲它很適合做數據存儲或 RPC 數據交換格式。可用於通訊協議、數據存儲等領域的語言無關、平臺無關、可擴展的序列化結構數據格式,用於將這些數據結構產生或解析數據流,並且性能比json/xml好。
核心設計
- MessageController
/**
* controller註解類,用來spring掃描生成handler消息處理
* 跟 MessageMapping 一起使用
*
* @see MessageMapping
* @see MessageDispatcher
* @author fomin
* @date 2019-12-07
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Component
public @interface MessageController {
}
- MessageMapping
/**
* 消息處理器,支持處理指定的消息,需要註解到public方法
*
* @author fomin
* @date 2019-12-07
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@MetaAnnotation(MessageResolverImp.class)
public @interface MessageMapping {
/**
* 消息類型
*/
int messageType();
}
- MessageDispatcher
/**
* 消息調度,掃描初始化MessageController註解類
*
* @author fomin
* @date 2019-12-07
*/
public class MessageDispatcher implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(@NotNull ContextRefreshedEvent event) {
if (initialized.compareAndSet(false, true)) {
init(event.getApplicationContext());
registerMapping(event.getApplicationContext());
}
}
/**
* 初始化 MessageDispatcher, 從 ApplicationContext 中掃描消息處理器並註冊
*/
private void init(ApplicationContext context) {
// 掃描標註MessageController的類
for (Object bean : context.getBeansWithAnnotation(MessageController.class).values()) {
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
for (Method method : targetClass.getMethods()) {
MessageData data = resolveMessageData(method);
if (data == null) continue;
if (handlers.containsKey(data)) {
throw new IllegalStateException("Duplicate handler data: " + data);
} else {
handlers.put(data, new MessageHandler(data, bean, objectStringifier));
log.debug("Registered handler: {}", data);
}
}
}
}
/**
* 註冊方法映射
*/
private void registerMapping(ApplicationContext context) {
try {
RequestMappingInfo mapping = RequestMappingInfo.paths(path).build();
Method method = this.getClass().getMethod("handleApi", HttpServletRequest.class, HttpServletResponse.class);
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
handlerMapping.registerMapping(mapping, this, method);
} catch (NoSuchMethodException e) {
throw new IllegalStateException(e);
}
}
// 執行相關的api請求
@SuppressWarnings("WeakerAccess")
public void handleApi(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
try {
MessageRequest request = messageFactory.parseMessageRequest(httpRequest);
log.info("[API] Request: {}", request);
ArgumentData argumentData = createArgumentData(httpRequest, httpResponse);
MessageResponse response = dispatchMessage(request, argumentData);
log.info("[API] Response: {}", response);
byte[] rawData = response.toByteArray();
byte[] sentData = gzip(httpRequest, httpResponse, rawData);
log.info("[API] Total " + sentData.length + " bytes sent, raw " + rawData.length + " bytes.");
httpResponse.setContentLength(sentData.length);
httpResponse.setContentType(response.getContentType());
httpResponse.getOutputStream().write(sentData);
httpResponse.getOutputStream().flush();
} catch (Throwable e) {
unexpectedException(httpResponse, e);
}
}
}
上面只展示部分核心代碼,具體代碼文末會說明,該框架的設計思路其實就是利用註解和反射方式,在spring容器初始化的時候掃描相關注解類,並把該類的相關注解方法掃描保存到Map中,最後定義handlerApi把利用spring-webmvc註冊到registerMapping中。
時序圖
使用
需要先定義一個PB的請求類型enum文件,並使用pb工具生產響應PB的java文件。
//定義額pb文件
enum PBMessageType {
DEFAULT_TYPE = 0;
}
// 生成的pb文件
/**
* Protobuf enum {@code fgame.PBMessageType}
*/
public enum PBMessageType
implements com.google.protobuf.ProtocolMessageEnum {
/**
* <code>DEFAULT_TYPE = 0;</code>
*/
DEFAULT_TYPE(0),
}
創建一個api-server的model,存放相關的controller文件
@MessageController
public class HttpController {
@MessageMapping(messageType = PBMessageType.DEFAULT_TYPE_VALUE)
public PBString adminLogin(PBWelcome req) {
return PBWelcome.newBuilder().setValue("Hello word!").build();
}
}
這樣就可以打造一個支持pb的http請求了。github地址:https://github.com/fomin-zhu/Fg-message