plugin封装增强

This commit is contained in:
黄华
2021-12-08 19:09:40 +08:00
parent 157d0018a8
commit a25a9d1969
12 changed files with 745 additions and 0 deletions

160
fizz-plugin/plugin-core.md Executable file
View File

@@ -0,0 +1,160 @@
---
home: false
title: plugin core
---
## 主要封装说明
- 1、引入核心包后不影响以前编写的插件只是提供更加便捷的开发方式
- 2、插件名字编写规范。在保留原有编写方式的前提下强制实现 pluginName() 方法,对开发更加友好,减少失误;
- 3、配置获取更加容易。配置主要有3个路由配置、插件全局配置、插件在路由里的个性配置。现在都可以直接获取相应的配置实体对象而不是默认提供的 Map 或者 String
## 使用说明
核心包是用于开发插件的基础包。主要是简化操作,方便开发,使开发人员更专注于业务代码对编写。
**1、编写2个配置实体类插件在路由里的个性配置、插件全局配置并添加注解 @FizzConfig**
> @FizzConfig 参数说明:
>
> contentParser :配置内容解析器。选填,默认是 json 解析器 JsonParser 。也可以自定义解析器,只需实现 we.plugin.core.filter.config.ContentParser 接口
注意:默认解析器 JsonParser 的 parseRouterCfg 方法只对第一层的 json string 做了增强,但这也足够用了。如(注意 varJson 是个 json 字符串,并不是 json 对象):
```groovy
void parseRouterCfg() {
String varJson = "{\n" +
" \"var1\": \"var1\",\n" +
" \"var2\": \"var2\",\n" +
" \"var3\": \"var3\"\n" +
"}";
// String varJson = "";
// String varJson = null;
Map<String, String> config = Maps.newHashMap();
config.put("codeSource", "this is code source");
config.put("var", varJson);
RouterConfig routerConfig = parser.parseRouterCfg(config, RouterConfig.class);
System.out.println(routerConfig);
}
```
示例:
```java
@Data
@FizzConfig
public class PluginConfig {
private String id;
private Var var;
}
@Data
@FizzConfig
public class RouterConfig {
private String codeSource;
private Var var;
}
@Data
public class Var {
private String var1;
private Integer var2;
private Long var3;
}
```
**3、编写插件逻辑**
继承 we.plugin.core.filter.AbstractFizzPlugin ,并实现 pluginName 和 doFilter 方法
> pluginName 方法:获取插件名称。无参,返回插件名称,要与网关后台配置的插件名称一致
>
> doFilter 方法:插件主要逻辑方法。入参是 ServerWebExchange出参是 Mono<Void>
直接调用父类方法获取各种配置:
> routerConfig获取路由级别插件配置
>
> pluginConfig获取插件级别插件配置
>
> originRouterCfg获取原始路由级别插件配置
>
> originPluginCfg获取原始插件级别插件配置
>
> apiConfig获取路由配置
示例:
```java
package we.fizz.plugin.example.plugin;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import we.plugin.core.filter.AbstractFizzPlugin;
import we.plugin.core.filter.config.FizzConfig;
import we.plugin.auth.ApiConfig;
import we.util.WebUtils;
import java.util.Map;
import static we.fizz.plugin.example.plugin.ExamplePlugin.PluginConfig;
import static we.fizz.plugin.example.plugin.ExamplePlugin.RouterConfig;
@Slf4j
@Component
public class ExamplePlugin extends AbstractFizzPlugin<RouterConfig, PluginConfig> {
/**
* 插件名称
*/
@Override
public String pluginName() {
return "examplePlugin";
}
/**
* filter逻辑
*/
@Override
public Mono<Void> doFilter(ServerWebExchange exchange) {
RouterConfig routerConfig = routerConfig(exchange);
PluginConfig pluginConfig = pluginConfig(exchange);
Map<String, Object> originRouterCfg = originRouterCfg(exchange);
String originPluginCfg = originPluginCfg(exchange);
ApiConfig apiConfig = apiConfig(exchange);
if (log.isTraceEnabled()) {
log.trace("routerConfig : {}", routerConfig);
log.trace("pluginConfig : {}", pluginConfig);
log.trace("originRouterCfg : {}", originRouterCfg);
log.trace("originPluginCfg : {}", originPluginCfg);
log.trace("apiConfig : {}", apiConfig);
}
return WebUtils.buildDirectResponse(exchange, HttpStatus.OK, null, "success");
}
@Data
@FizzConfig
public static class PluginConfig {
private String id;
private Var var;
}
@Data
public static class Var {
private String var1;
private String var2;
private String var3;
}
@Data
@FizzConfig
public static class RouterConfig {
private String codeSource;
private Var var;
}
}
```

View File

@@ -0,0 +1,26 @@
package we.plugin.core.config;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import we.plugin.core.filter.config.parser.JsonParser;
import we.plugin.core.spring.FizzPluginAliasProcessor;
/**
* @author huanghua
*/
@Configuration
@ComponentScan({"we.config", "we.fizz", "we.plugin", "we.filter", "we.proxy", "we.stats"/*, "we.plugin.core"*/})
public class WeFizzPluginAutoConfiguration {
@Bean
public FizzPluginAliasProcessor fizzPluginAliasProcess(ApplicationContext context) {
return new FizzPluginAliasProcessor(context);
}
@Bean
public JsonParser jsonParser() {
return new JsonParser();
}
}

View File

@@ -0,0 +1,144 @@
package we.plugin.core.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import we.plugin.FizzPluginFilter;
import we.plugin.PluginConfig;
import we.plugin.auth.ApiConfig;
import we.plugin.auth.ApiConfigService;
import we.plugin.core.filter.config.ContentParser;
import we.plugin.core.filter.config.FizzConfig;
import we.plugin.core.filter.config.parser.JsonParser;
import we.plugin.core.spring.util.FizzPluginAppContextUtils;
import we.util.WebUtils;
import javax.annotation.Resource;
import java.lang.reflect.ParameterizedType;
import java.util.Map;
import java.util.function.Function;
/**
* @author huanghua
*/
@Slf4j
@SuppressWarnings("unchecked")
public abstract class AbstractFizzPlugin<RouterCfg, PluginCfg> implements FizzPluginFilter {
// api 配置
public final Function<String, String> nameExApiCfg = in -> "fizz.pl.api.cfg";
// 路由上的插件配置(原始)
public final Function<String, String> nameExRtCfg = in -> "fizz.pl.rt.cfg." + pluginName();
// 插件级别配置(原始)
public final Function<String, String> nameExPlCfg = in -> "fizz.pl.pl.cfg." + pluginName();
// 路由上的插件配置(解析后)
public final Function<String, String> nameExRtCfgParsed = in -> "fizz.pl.rt.cfg.parsed." + pluginName();
// 插件级别配置(解析后)
public final Function<String, String> nameExPlCfgParsed = in -> "fizz.pl.pl.cfg.parsed." + pluginName();
@Resource
private ApiConfigService apiConfigService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, Map<String, Object> config) {
if (log.isTraceEnabled()) {
log.trace("{} doFilter execute...", this.pluginName());
}
initConfig(exchange, config);
return this.doFilter(exchange);
}
/**
* 获取路由级别插件配置
*/
public RouterCfg routerConfig(ServerWebExchange exchange) {
if (originRouterCfg(exchange) == null) {
return null;
}
RouterCfg routerCfgInAttr = exchange.getAttribute(nameExRtCfgParsed.apply(pluginName()));
if (routerCfgInAttr != null) {
return routerCfgInAttr;
}
Class<RouterCfg> cfgClass = (Class<RouterCfg>) ((ParameterizedType) getClass().getGenericSuperclass())
.getActualTypeArguments()[0];
FizzConfig fizzConfig = AnnotationUtils.findAnnotation(cfgClass, FizzConfig.class);
Class<? extends ContentParser> cfgParser = fizzConfig == null ? JsonParser.class : fizzConfig.contentParser();
RouterCfg routerCfg = FizzPluginAppContextUtils.getBean(cfgParser).parseRouterCfg(originRouterCfg(exchange), cfgClass);
putAttr2exchange(exchange, nameExRtCfgParsed.apply(pluginName()), routerCfg);
return routerCfg;
}
/**
* 获取插件级别插件配置
*/
public PluginCfg pluginConfig(ServerWebExchange exchange) {
if (originPluginCfg(exchange) == null) {
return null;
}
PluginCfg pluginCfgInAttr = exchange.getAttribute(nameExPlCfgParsed.apply(pluginName()));
if (pluginCfgInAttr != null) {
return pluginCfgInAttr;
}
Class<PluginCfg> cfgClass = (Class<PluginCfg>) ((ParameterizedType) getClass().getGenericSuperclass())
.getActualTypeArguments()[1];
FizzConfig fizzConfig = AnnotationUtils.findAnnotation(cfgClass, FizzConfig.class);
Class<? extends ContentParser> cfgParser = fizzConfig == null ? JsonParser.class : fizzConfig.contentParser();
PluginCfg pluginCfg = FizzPluginAppContextUtils.getBean(cfgParser).parsePluginCfg(originPluginCfg(exchange), cfgClass);
putAttr2exchange(exchange, nameExPlCfgParsed.apply(pluginName()), pluginCfg);
return pluginCfg;
}
/**
* 获取原始路由级别插件配置
*/
public <T> T originRouterCfg(ServerWebExchange exchange) {
return exchange.getAttribute(nameExRtCfg.apply(pluginName()));
}
/**
* 获取原始插件级别插件配置
*/
public <T> T originPluginCfg(ServerWebExchange exchange) {
return exchange.getAttribute(nameExPlCfg.apply(pluginName()));
}
/**
* 获取路由配置
*/
public ApiConfig apiConfig(ServerWebExchange exchange) {
return exchange.getAttribute(nameExApiCfg.apply(pluginName()));
}
protected void putAttr2exchange(ServerWebExchange exchange, String key, Object val) {
if (exchange == null || key == null || val == null) {
return;
}
exchange.getAttributes().put(key, val);
}
private void initConfig(ServerWebExchange exchange, Map<String, Object> config) {
ServerHttpRequest req = exchange.getRequest();
ApiConfig apiConfig = apiConfigService.getApiConfig(WebUtils.getAppId(exchange),
WebUtils.getClientService(exchange), req.getMethod(), WebUtils.getClientReqPath(exchange));
String fixedConfig = (String) config.get(PluginConfig.CUSTOM_CONFIG);
if (log.isTraceEnabled()) {
log.trace("api config : {}", apiConfig);
log.trace("router config : {}", config);
log.trace("plugin config : {}", fixedConfig);
}
putAttr2exchange(exchange, nameExApiCfg.apply(pluginName()), apiConfig);
putAttr2exchange(exchange, nameExRtCfg.apply(pluginName()), config);
putAttr2exchange(exchange, nameExPlCfg.apply(pluginName()), fixedConfig);
}
/**
* 插件名称
*/
public abstract String pluginName();
/**
* filter逻辑
*/
public abstract Mono<Void> doFilter(ServerWebExchange exchange);
}

View File

@@ -0,0 +1,39 @@
package we.plugin.core.filter.config;
import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.StringUtils;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author huanghua
*/
public abstract class ConfigUtils {
public static final String DEFAULT_CHAR_MATCHER_ANY_OF = ",\n";
public static Set<String> string2set(String strVal, String charMatcherAnyOf) {
Set<String> finalSet = Sets.newHashSet();
if (StringUtils.isBlank(strVal)) {
return finalSet;
}
charMatcherAnyOf = StringUtils.isBlank(charMatcherAnyOf) ? DEFAULT_CHAR_MATCHER_ANY_OF : charMatcherAnyOf;
Set<String> set = Sets.newHashSet(
Splitter.on(CharMatcher.anyOf(charMatcherAnyOf)).trimResults().split(strVal));
set = set.stream().filter(StringUtils::isNotBlank).collect(Collectors.toSet());
for (String s : set) {
if (StringUtils.isBlank(s)) {
continue;
}
finalSet.add(StringUtils.trimToEmpty(s));
}
return finalSet;
}
public static Set<String> string2set(String strVal) {
return string2set(strVal, DEFAULT_CHAR_MATCHER_ANY_OF);
}
}

View File

@@ -0,0 +1,18 @@
package we.plugin.core.filter.config;
import java.util.Map;
/**
* @author huanghua
*/
public interface ContentParser {
default <T> T parseRouterCfg(Map<String, Object> config, Class<T> toValueType) {
throw new RuntimeException();
}
default <T> T parsePluginCfg(String source, Class<T> toValueType) {
throw new RuntimeException();
}
}

View File

@@ -0,0 +1,21 @@
package we.plugin.core.filter.config;
import we.plugin.core.filter.config.parser.JsonParser;
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.TYPE;
/**
* @author huanghua
*/
@Target(TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface FizzConfig {
/**
* 配置内容解析器
*/
Class<? extends ContentParser> contentParser() default JsonParser.class;
}

View File

@@ -0,0 +1,90 @@
package we.plugin.core.filter.config.parser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import we.plugin.core.filter.config.ContentParser;
import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author huanghua
*/
@Slf4j
public class JsonParser implements ContentParser {
private static final Set<Class<?>> IGNORE_CONVERT_CLASS = Sets.newHashSet(
String.class
, Long.class
, Integer.class
, Double.class
, Short.class
, CharSequence.class
, Character.class
, BigDecimal.class
, Boolean.class
);
private static final Map<Class<?>, List<Field>> FIELD_CACHE = Maps.newHashMap();
@Resource
private ObjectMapper objectMapper;
@Override
public <T> T parseRouterCfg(Map<String, Object> config, Class<T> toValueType) {
ObjectNode jsonNode = objectMapper.convertValue(config, ObjectNode.class);
convertConfig(jsonNode, toValueType);
return objectMapper.convertValue(jsonNode, toValueType);
}
@Override
public <T> T parsePluginCfg(String source, Class<T> toValueType) {
try {
return objectMapper.readValue(source, toValueType);
} catch (JsonProcessingException e) {
log.warn(e.getMessage(), e);
throw new RuntimeException(e.getMessage(), e);
}
}
private void convertConfig(ObjectNode jsonNode, Class<?> toValueType) {
List<Field> cacheFields = fields(toValueType);
for (Field field : cacheFields) {
String fn = field.getName();
if (jsonNode.has(fn) && !IGNORE_CONVERT_CLASS.contains(field.getType())) {
List<Field> fs = fields(field.getType());
if (fs.size() > 0) {
JsonNode node = jsonNode.get(fn);
JsonNode readTree = null;
try {
readTree = objectMapper.readTree(node.asText());
} catch (JsonProcessingException e) {
// ignore
}
if (readTree != null) {
jsonNode.put(fn, readTree);
}
}
}
}
}
private List<Field> fields(Class<?> toValueType) {
List<Field> cacheFields = FIELD_CACHE.get(toValueType);
if (cacheFields == null) {
// returns all members including private members but not inherited members.
Field[] fields = toValueType.getDeclaredFields();
cacheFields = Lists.newArrayList(fields);
FIELD_CACHE.put(toValueType, cacheFields);
}
return cacheFields;
}
}

View File

@@ -0,0 +1,51 @@
package we.plugin.core.spring;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.util.CollectionUtils;
import we.plugin.core.filter.AbstractFizzPlugin;
import javax.annotation.PostConstruct;
import java.util.Map;
/**
* @author huanghua
*/
@Slf4j
public class FizzPluginAliasProcessor {
private ApplicationContext applicationContext;
public FizzPluginAliasProcessor(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@PostConstruct
public void postProcessPluginAlias() {
Map<String, AbstractFizzPlugin> serviceBeanMap = applicationContext.getBeansOfType(AbstractFizzPlugin.class);
if (CollectionUtils.isEmpty(serviceBeanMap)) {
log.debug("not found fizz plugin. skip!");
return;
}
if (!(applicationContext instanceof GenericApplicationContext)) {
log.error("ApplicationContext is not instance of GenericApplicationContext. skip!");
return;
}
serviceBeanMap.forEach((s, o) -> registerAlias(
((GenericApplicationContext) applicationContext).getBeanFactory(), s, o));
}
private void registerAlias(ConfigurableListableBeanFactory beanFactory,
String beanName, AbstractFizzPlugin fizzPlugin) {
log.debug("register bean : {}", fizzPlugin.getClass().getName());
BeanDefinition bd = beanFactory.getBeanDefinition(beanName);
if (bd instanceof AbstractBeanDefinition) {
AbstractBeanDefinition abd = (AbstractBeanDefinition) bd;
abd.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_AUTODETECT);
}
beanFactory.registerAlias(beanName, fizzPlugin.pluginName());
}
}

View File

@@ -0,0 +1,34 @@
package we.plugin.core.spring.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* @author huanghua
* @deprecated and use {@link we.Fizz} instead
*/
@Component
public class FizzPluginAppContextUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext appContext) throws BeansException {
applicationContext = appContext;
}
public static <T> T getBean(Class<T> requiredType) throws BeansException {
return FizzPluginAppContextUtils.getApplicationContext().getBean(requiredType);
}
public static ApplicationContext getApplicationContext() {
if (applicationContext == null) {
String msg = "The applicationContext is not yet available. "
+ "Please ensure that the spring applicationContext is completely created before calling this method!";
throw new IllegalStateException(msg);
}
return applicationContext;
}
}

View File

@@ -0,0 +1,76 @@
package we.plugin.myplugin;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import we.plugin.auth.ApiConfig;
import we.plugin.core.filter.AbstractFizzPlugin;
import we.plugin.core.filter.config.FizzConfig;
import we.util.WebUtils;
import java.util.Map;
@Slf4j
@Component
public class MyPluginPlus extends AbstractFizzPlugin<MyPluginPlus.RouterConfig, MyPluginPlus.PluginConfig> {
/**
* 插件名称
*/
@Override
public String pluginName() {
return "myPluginPlus";
}
/**
* filter逻辑
*/
@Override
public Mono<Void> doFilter(ServerWebExchange exchange) {
RouterConfig routerConfig = routerConfig(exchange);
PluginConfig pluginConfig = pluginConfig(exchange);
Map<String, Object> originRouterCfg = originRouterCfg(exchange);
String originPluginCfg = originPluginCfg(exchange);
ApiConfig apiConfig = apiConfig(exchange);
if (log.isTraceEnabled()) {
log.trace("routerConfig : {}", routerConfig);
log.trace("pluginConfig : {}", pluginConfig);
log.trace("originRouterCfg : {}", originRouterCfg);
log.trace("originPluginCfg : {}", originPluginCfg);
log.trace("apiConfig : {}", apiConfig);
}
return WebUtils.buildDirectResponse(exchange, HttpStatus.OK, null, "success");
}
@Data
@FizzConfig
public static class PluginConfig {
private String id;
private PluginItem pluginItem;
}
@Data
public static class PluginItem {
private String p1;
private String p2;
private String p3;
}
@Data
@FizzConfig
public static class RouterConfig {
private String codeSource;
private RouterItem routerItem;
}
@Data
public static class RouterItem {
private String r1;
private String r2;
private String r3;
}
}

View File

@@ -0,0 +1,3 @@
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
we.plugin.core.config.WeFizzPluginAutoConfiguration

View File

@@ -0,0 +1,83 @@
package we.plugin.core.filter.config.parser;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Maps;
import lombok.Data;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import we.plugin.core.filter.config.FizzConfig;
import java.lang.reflect.Field;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class JsonParserTest {
private static JsonParser parser = new JsonParser();
@BeforeAll
public static void init() throws NoSuchFieldException, IllegalAccessException {
Class<?> clazz = parser.getClass();
Field field = clazz.getDeclaredField("objectMapper");
field.setAccessible(true);
field.set(parser, new ObjectMapper());
}
@Test
void parseRouterCfg() {
String varJson = "{\n" +
" \"var1\": \"var1\",\n" +
" \"var2\": \"var2\",\n" +
" \"var3\": \"var3\"\n" +
"}";
// String varJson = "";
// String varJson = null;
Map<String, Object> config = Maps.newHashMap();
config.put("codeSource", "this is code source");
config.put("var", varJson);
RouterConfig routerConfig = parser.parseRouterCfg(config, RouterConfig.class);
assertNotNull(routerConfig, "未解析出routerConfig");
assertNotNull( routerConfig.getVar(), "未解析出routerConfig.var");
assertEquals("var1", routerConfig.getVar().getVar1(), "routerConfig.var.var1不匹配");
}
@Test
void parsePluginCfg() {
String json = "{\n" +
" \"id\": \"123\",\n" +
" \"var\": {\n" +
" \"var1\": \"var1\",\n" +
" \"var2\": \"var2\",\n" +
" \"var3\": \"var3\"\n" +
" }\n" +
"}";
PluginConfig config = parser.parsePluginCfg(json, PluginConfig.class);
assertNotNull(config, "未解析出config");
assertEquals("123", config.getId(), "id不匹配");
}
@Data
@FizzConfig
public static class PluginConfig {
private String id;
private Var var;
}
@Data
public static class Var {
private String var1;
private String var2;
private String var3;
}
@Data
@FizzConfig
public static class RouterConfig {
private String codeSource;
private Var var;
}
}