暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

JAVA中间件(middleware)模式

halouha 2021-04-18
2875

主要围绕下面几点介绍

  • 概念

  • 应用场景(对比其他语言)

  • 和filter,拦截器,传统的pipeline模式的区别(用forEach代替不了么?)

  • 在java中如何实现中间件模式(附源码)

中间件的概念

首先它是一种设计模式,一种功能的封装方式,就是封装在程序中处理复杂业务逻辑的功能。

说概念很难理解,结合应用场景比较好说明

比如在http请求中往往会涉及很多动作, IP筛选, 查询字符串传递, 请求体解析, cookie信息处理, 权限校验, 日志记录 ,会话管理中间件(session), gzip压缩中间件(如compress) ,错误处理等

aspnetcore中处理http请求的也是用的中间件模式, 

微软官方文档介绍:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-5.0

nodejs中封装以上http请求细节处理的方法里面用到的就是中间件模式。比如主流的nodejs框架 express ,koa 等

以koa框架为例 middleware的构成是用use方法添加, 下面的代码中添加了一个 日志中间件,一个在response的header中添加'X-Response-Time'(记录请求处理时长)的中间件

const Koa = require('koa');
const app = new Koa();

// logger

app.use(async (ctx, next) => {
await next();//进入下一个中间件
const rt = ctx.response.get('X-Response-Time');
//记录日志
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});

// x-response-time

app.use(async (ctx, next) => {
const start = Date.now();
await next();//进入下一个中间件
const ms = Date.now() - start;
//记录请求时长
ctx.set('X-Response-Time', `${ms}ms`);
});

// response

app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);


理解下意思:

  • 程序启动监听了并提供3000端口提供web服务

  • 你在浏览器输入http://localhost:3000并回车

  • 首先进入第一个logger中间件

  • logger中间件首先调用了next() 进入了下一个 x-response-time中间件

  • 记录当前的时间 调用next() 进入了 reponse中间件,返回 helloword

  • 回到x-response-time中间件记录时长

  • 回到logger中间件记录日志

  • 请求结束

仔细注意看:

中间件有一个next()函数,如果不调用next函数,请求就在这个中间件中终止了

这点也是中间件相比filter或者传统的pipeline对比最大的不同点(控制权交给了中间件本身)

filter的话 一旦开始走过滤器逻辑,要么全部走完


//java伪代码

for (Filter filter : myFilters){
filter.excute(xx)
}

//要么是在组织调用的地方加判断
for (Filter filter : myFilters){
bool continueRun = filter.excute(xx)
if(!continueRun) break;
}

每个filter都是独立执行的,在流程上无法过多干预,没有中间件模式灵活:

用极少的操作就能得到一个插件,用最简单的方法就能将新的过滤器和处理程序扩展到现有的系统上。

中间件模式运行示意图:

有点类似于【娃娃里面的娃娃】

在java中如何实现中间件模式

中间件模式中,最基础的组成部分就是 中间件管理器,我们可以用它来组织和执行中间件的函数,如图所示:

要实现中间件模式

  • 可以通过use()函数来注册新的中间件

  • 每个中间件的调用参数都是一致的

  • 每个中间件按照注册的顺序被依次调用

  • 每个中间件都有权停止进一步处理

下面我们动手封装下

我们定义一个中间件函数式接口

@FunctionalInterface
public interface Middleware {
void excute(MiddlewareContext ctx, MiddlewareNext next);
}


有2个参数

  • 中间件执行的上下文

  • 下一个中间件执行委托

/**
* 中间件方法的参数
* 上下文
*/

public class MiddlewareContext {
private final Map<String,Object> contextMapObjectCache;

public MiddlewareContext() {
contextMapObjectCache = new HashMap<>();
}
public <T> void set(String key,T value) {
contextMapObjectCache.put(key,value);
}
public <T> T get(String key){
if(contextMapObjectCache.containsKey(key)){
return (T)contextMapObjectCache.get(key);
}
return null;
}
}

//下一个中间件委托

@FunctionalInterface

public interface MiddlewareNext {
void excute();
}


中间件管理器


/**
* 中间件管理器
*/

public class MiddlewareBuilder {
private final List<Middleware> middlewareList;

public MiddlewareBuilder() {
this.middlewareList = new ArrayList<>();
}

public MiddlewareBuilder use(Middleware middleware) {
this.middlewareList.add(middleware);
return this;
}

public Consumer<MiddlewareContext> build(){
MiddlewarePipeline pipeline = null;
Collections.reverse(middlewareList);
for (Middleware middleware : middlewareList) {
pipeline = pipeline == null ? new MiddlewarePipeline(middleware) : pipeline.addHandler(middleware);
}
return pipeline::excute;
}

private static class MiddlewarePipeline {
private final Middleware currentHandler;
public MiddlewarePipeline(Middleware currentHandler) {
this.currentHandler = currentHandler;
}
public MiddlewarePipeline addHandler(Middleware newHandler) {
return new MiddlewarePipeline((input, next1) -> {
MiddlewareNext next2 = () -> currentHandler.excute(input, next1);
newHandler.excute(input, next2);
});
}
public void excute(MiddlewareContext ctx){
this.currentHandler.excute(ctx,()->{});
}
}
}


以上总共代码不超过60行实现了java中间件模式的封装,

对比nodejs或者c#中使用方式基本保持一致,代码高度简洁。

使用中间件

//创建一个中间件构造器

MiddlewareBuilder app = new MiddlewareBuilder();


//添加中间件

app.use((ctx, next) -> {
System.out.println("middle-1--->start");
next.excute(); //进入下一个中间件
System.out.println("middle-1--->end");
});


//添加中间件

app.use((ctx, next) -> {
System.out.println("middle-2--->start");
long startTime = System.currentTimeMillis();
next.excute(); //进入下一个中间件
long rt = (System.currentTimeMillis() - startTime);
ctx.set("X-Response-Time", rt);
System.out.println("middle-2--->end");
});


//添加中间件

app.use((ctx, next) -> {
System.out.println("middle-3--->start");
ctx.set("body", "Hello World");
System.out.println("middle-3--->end");
});


//执行中间件

app.build().accept(new MiddlewareContext());


执行结果,符合预期:

middle-1--->start
middle-2--->start
middle-3--->start
middle-3--->end
middle-2--->end
middle-1--->end


在实际项目中还可以进一步结合spring的特性来封装一个Middleware注解

新增一个spring后置处理器

@Component
public class MiddlewarePointAnnotationProcessor implements BeanPostProcessor {
private final ConfigurableListableBeanFactory configurableBeanFactory;

@Autowired
public MiddlewarePointAnnotationProcessor(ConfigurableListableBeanFactory beanFactory) {
this.configurableBeanFactory = beanFactory;
}

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
this.scanAnnotation(bean, beanName);
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}

protected void scanAnnotation(Object bean, String beanName) {
this.configureFieldInjection(bean);
}

private void configureFieldInjection(Object bean) {
Class<?> managedBeanClass = bean.getClass();
ReflectionUtils.FieldCallback fieldCallback =
new MiddlewarePointFieldCallback(configurableBeanFactory, bean);
ReflectionUtils.doWithFields(managedBeanClass, fieldCallback);
}
}


在springboot中使用的时候就更加方便了如下图:



我是正东,学的越多不知道也越多。这个公众号是我的实验小天地,我会分享一些我开源的工具(欢迎你来提意见),好玩好用的新技术。如果你也和我一样喜欢折腾技术请关注 !


文章转载自halouha,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论