Spring Boot 中间件开发(一)《服务治理中间件之统一白名单验证》

本文涉及的产品
注册配置 MSE Nacos/ZooKeeper,118元/月
性能测试 PTS,5000VUM额度
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: Spring Boot + 领域驱动设计使得微服务越来越火热,而随着微服务越来越多,服务的治理就显得尤为重要。在我们的业务领域开发中,经常会有一些通用性功能搭建,比如;白名单、黑名单、限流、熔断等,为了更好的开发业务功能,我们需要将非业务功能的通用逻辑提取出来开发出通用组件,以便于业务系统使用。而不至于Copy来Copy去,让代码乱的得加薪才能修改的地步!

前言介绍

Spring Boot + 领域驱动设计使得微服务越来越火热,而随着微服务越来越多,服务的治理就显得尤为重要。

在我们的业务领域开发中,经常会有一些通用性功能搭建,比如;白名单、黑名单、限流、熔断等,为了更好的开发业务功能,我们需要将非业务功能的通用逻辑提取出来开发出通用组件,以便于业务系统使用。而不至于Copy来Copy去,让代码乱的得加薪才能修改的地步!

通常一个中间件开发会需要用到;自定义xml配置、自定义Annotation注解、动态代理、反射调用、字节码编程(javaassist、ASM等),以及一些动态注册服务中心和功能逻辑开发等。本案例会使用Spring Boot开发方式定义自己的starter。

原理简述

通过我们使用一个公用的starter的时候,只需要将相应的依赖添加的Maven的配置文件当中即可,免去了自己需要引用很多依赖类,并且SpringBoot会自动进行类的自动配置。而我们自己开发一个starter也需要做相应的处理;

  1. SpringBoot 在启动时会去依赖的starter包中寻找 resources/META-INF/spring.factories 文件,然后根据文件中配置的Jar包去扫描项目所依赖的Jar包,这类似于 Java 的 SPI 机制。

SPI 全称 Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。

  1. 根据 spring.factories配置加载AutoConfigure类。
  2. 根据 @Conditional注解的条件,进行自动配置并将Bean注入Spring Context 上下文当中。也可以使用@ImportAutoConfiguration({MyServiceAutoConfiguration.class}) 指定自动配置哪些类。
  3. 日常使用的Spring官方的Starter一般采取spring-boot-starter-{name} 的命名方式,如 spring-boot-starter-web 。而非官方的Starter,官方建议 artifactId 命名应遵循{name}-spring-boot-starter 的格式。 例如:door-spring-boot-starter 。

环境准备

  1. jdk 1.8.0
  2. Maven 3.x
  3. IntelliJ IDEA Community Edition 2018.3.1 x64

工程示例

中间件工程:door-spring-boot-starter

1door-spring-boot-starter
 2└── src
 3    ├── main
 4    │   ├── java
 5    │   │   └── org.itstack.door
 6    │   │       ├── annotation
 7    │   │       │    └── DoDoor.java 
 8    │   │       ├── config
 9    │   │       │    ├── StarterAutoConfigure.java   
10    │   │       │    ├── StarterService.java 
11    │   │       │    └── StarterServiceProperties.java   
12    │   │       └── DoJoinPoint.java
13    │   └── resources    
14    │       └── META_INF
15    │           └── spring.factories    
16    └── test
17        └── java
18            └── org.itstack.demo.test
19                └── ApiTest.java

演示部分重点代码块,完整代码下载关注公众号;bugstack虫洞栈,回复:中间件开发

door/annotation/DoDoor.java & 自定义注解

  • 自定义注解,用于AOP切面
  • key;获取入参类属性中某个值
  • returnJson;拦截返回Json内容
1@Retention(RetentionPolicy.RUNTIME)
2@Target(ElementType.METHOD)
3public @interface DoDoor {
4
5    String key() default "";
6
7    String returnJson() default "";
8
9}

config/StarterAutoConfigure.java & 配置信息装配

  • 通过注解;@Configuration、@ConditionalOnClass、@EnableConfigurationProperties,来实现自定义配置获取值
  • prefix = "itstack.door",用于在yml中的配置
1@Configuration
 2@ConditionalOnClass(StarterService.class)
 3@EnableConfigurationProperties(StarterServiceProperties.class)
 4public class StarterAutoConfigure {
 5
 6    @Autowired
 7    private StarterServiceProperties properties;
 8
 9    @Bean
10    @ConditionalOnMissingBean
11    @ConditionalOnProperty(prefix = "itstack.door", value = "enabled", havingValue = "true")
12    StarterService starterService() {
13        return new StarterService(properties.getUserStr());
14    }
15
16}

config/StarterServiceProperties.java & 属性配置

  • @ConfigurationProperties("itstack.door"),注解获取配置
  • userStr白名单用户
1@ConfigurationProperties("itstack.door")
 2public class StarterServiceProperties {
 3
 4    private String userStr;
 5
 6    public String getUserStr() {
 7        return userStr;
 8    }
 9
10    public void setUserStr(String userStr) {
11        this.userStr = userStr;
12    }
13
14}

DoJoinPoint.java & 自定义切面

  • 自定义切面获取方法和属性值
  • 通过属性值判断此用户ID是否属于白名单范围
  • 属于白名单则放行通过jp.proceed();
  • 对于拦截的用于需要通过returnJson反序列为对象返回
1@Aspect
 2@Component
 3public class DoJoinPoint {
 4
 5    private Logger logger = LoggerFactory.getLogger(DoJoinPoint.class);
 6
 7    @Autowired
 8    private StarterService starterService;
 9
10    @Pointcut("@annotation(org.itstack.door.annotation.DoDoor)")
11    public void aopPoint() {
12    }
13
14    @Around("aopPoint()")
15    public Object doRouter(ProceedingJoinPoint jp) throws Throwable {
16        //获取内容
17        Method method = getMethod(jp);
18        DoDoor door = method.getAnnotation(DoDoor.class);
19        //获取字段值
20        String keyValue = getFiledValue(door.key(), jp.getArgs());
21        logger.info("itstack door handler method:{} value:{}", method.getName(), keyValue);
22        if (null == keyValue || "".equals(keyValue)) return jp.proceed();
23        //配置内容
24        String[] split = starterService.split(",");
25        //白名单过滤
26        for (String str : split) {
27            if (keyValue.equals(str)) {
28                return jp.proceed();
29            }
30        }
31        //拦截
32        return returnObject(door, method);
33    }
34
35    private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
36        Signature sig = jp.getSignature();
37        MethodSignature methodSignature = (MethodSignature) sig;
38        return getClass(jp).getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
39    }
40
41    private Class<? extends Object> getClass(JoinPoint jp) throws NoSuchMethodException {
42        return jp.getTarget().getClass();
43    }
44
45    //返回对象
46    private Object returnObject(DoDoor doGate, Method method) throws IllegalAccessException, InstantiationException {
47        Class<?> returnType = method.getReturnType();
48        String returnJson = doGate.returnJson();
49        if ("".equals(returnJson)) {
50            return returnType.newInstance();
51        }
52        return JSON.parseObject(returnJson, returnType);
53    }
54
55    //获取属性值
56    private String getFiledValue(String filed, Object[] args) {
57        String filedValue = null;
58        for (Object arg : args) {
59            try {
60                if (null == filedValue || "".equals(filedValue)) {
61                    filedValue = BeanUtils.getProperty(arg, filed);
62                } else {
63                    break;
64                }
65            } catch (Exception e) {
66                if (args.length == 1) {
67                    return args[0].toString();
68                }
69            }
70        }
71        return filedValue;
72    }
73
74}

pom.xml & 部分配置内容

  • 中间件开发用到了切面,因此需要引入spring-boot-starter-aop
  • 为了使调用端不用关心中间件都引入那些包,可以将额外的包一起打包给中间件
1<dependency>
 2    <groupId>org.springframework.boot</groupId>
 3    <artifactId>spring-boot-starter-aop</artifactId>
 4</dependency>
 5
 6<plugin>
 7    <groupId>org.apache.maven.plugins</groupId>
 8    <artifactId>maven-jar-plugin</artifactId>
 9    <version>2.3.2</version>
10    <configuration>
11        <archive>
12            <addMavenDescriptor>false</addMavenDescriptor>
13            <index>true</index>
14            <manifest>
15                <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
16                <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
17            </manifest>
18            <manifestEntries>
19                <Implementation-Build>${maven.build.timestamp}</Implementation-Build>
20            </manifestEntries>
21        </archive>
22    </configuration>
23</plugin>

spring.factories & spring入口配置

  • 将自己的XxxConfigue配置到这里,用于spring启动时候扫描到
1org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.itstack.door.config.StarterAutoConfigure

测试工程:itstack-demo-springboot-helloworld

1itstack-demo-springboot-helloworld
 2└── src
 3    ├── main
 4    │   ├── java
 5    │   │   └── org.itstack.demo
 6    │   │       ├── domain
 7    │   │       │    └── UserInfo.java   
 8    │   │       ├── web    
 9    │   │       │    └── HelloWorldController.java   
10    │   │       └── HelloWorldApplication.java
11    │   └── resources    
12    │       └── application.yml    
13    └── test
14        └── java
15            └── org.itstack.demo.test
16                └── ApiTest.java

演示部分重点代码块,完整代码下载关注公众号;bugstack虫洞栈,回复:中间件开发

pom.xml & 引入中间件配置

1<dependency>
2    <groupId>org.itatack.demo</groupId>
3    <artifactId>door-spring-boot-starter</artifactId>
4    <version>1.0.1-SNAPSHOT</version>
5</dependency>

web/HelloWorldController.java & 配置白名单拦截服务

  • 在需要拦截的方法上添加@DoDoor注解;@DoDoor(key = "userId", returnJson = "{\"code\":\"1111\",\"info\":\"非白名单可访问用户拦截!\"}")
  • key;需要从入参取值的属性字段,如果是对象则从对象中取值,如果是单个值则直接使用
  • returnJson;预设拦截时返回值,是返回对象的Json
1@RestController
 2public class HelloWorldController {
 3
 4    @DoDoor(key = "userId", returnJson = "{\"code\":\"1111\",\"info\":\"非白名单可访问用户拦截!\"}")
 5    @RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
 6    public UserInfo queryUserInfo(@RequestParam String userId) {
 7        return new UserInfo("虫虫:" + userId, 19, "天津市南开区旮旯胡同100号");
 8    }
 9
10}

application.yml & Yml配置

  • 添加白名单配置,英文逗号隔开
1server:
 2  port: 8080
 3
 4spring:
 5  application:
 6    name: itstack-demo-springboot-helloworld
 7
 8# 自定义中间件配置
 9itstack:
10  door:
11    enabled: true
12    userStr: 1001,aaaa,ccc #白名单用户ID,多个逗号隔开

测试验证

  1. 启动工程(可以Debug调试);itstack-demo-springboot-helloworld
  2. 访问连接;
  3. 白名单用户:http://localhost:8080/api/queryUserInfo?userId=1001
    java {"code":"0000","info":"success","name":"虫虫:1001","age":19,"address":"天津市南开区旮旯胡同100号"}
  4. 非名单用户:http://localhost:8080/api/queryUserInfo?userId=小团团
    java {"code":"1111","info":"非白名单可访问用户拦截!","name":null,"age":null,"address":null}
  5. 服务度日志;
1  .   ____          _            __ _ _
 2 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
 3( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 4 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
 5  '  |____| .__|_| |_|_| |_\__, | / / / /
 6 =========|_|==============|___/=/_/_/_/
 7 :: Spring Boot ::        (v2.1.2.RELEASE)
 8
 92019-12-03 23:25:40.128  INFO 177110 --- [           main] org.itstack.demo.HelloWorldApplication   : Starting HelloWorldApplication on FUZHENGWEI with PID 177110 (E:\itstack\github.com\itstack-demo-springboot-helloworld\target\classes started by fuzhengwei in E:\itstack\github.com\itstack-demo-springboot-helloworld)
102019-12-03 23:25:40.133  INFO 177110 --- [           main] org.itstack.demo.HelloWorldApplication   : No active profile set, falling back to default profiles: default
112019-12-03 23:25:42.446  INFO 177110 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
122019-12-03 23:25:42.471  INFO 177110 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
132019-12-03 23:25:42.471  INFO 177110 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.14]
142019-12-03 23:25:42.483  INFO 177110 --- [           main] o.a.catalina.core.AprLifecycleListener   : The APR based Apache Tomcat Native library which allows optimal performance in
152019-12-03 23:25:42.611  INFO 177110 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
162019-12-03 23:25:42.612  INFO 177110 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2421 ms
172019-12-03 23:25:43.063  INFO 177110 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
182019-12-03 23:25:43.317  INFO 177110 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
192019-12-03 23:25:43.320  INFO 177110 --- [           main] org.itstack.demo.HelloWorldApplication   : Started HelloWorldApplication in 3.719 seconds (JVM running for 4.294)
202019-12-03 23:26:56.107  INFO 177110 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
212019-12-03 23:26:56.107  INFO 177110 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
222019-12-03 23:26:56.113  INFO 177110 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 6 ms
232019-12-03 23:26:56.171  INFO 177110 --- [nio-8080-exec-1] org.itstack.door.DoJoinPoint             : itstack door handler method:queryUserInfo value:1001
242019-12-03 23:27:04.090  INFO 177110 --- [nio-8080-exec-3] org.itstack.door.DoJoinPoint             : itstack door handler method:queryUserInfo value:小团团
25

综上总结

  • 此版本中间件还只是一个功能非常简单的雏形,后续还需继续拓展。比如;白名单用户自动更新、黑名单、熔断、降级、限流等。
  • 中间件开发可以将很多重复性工作抽象后进行功能整合,以提升我们使用工具的效率。
  • 鉴于Spring Boot是比较的趋势,我会不断的深挖以及开发一些服务组件。锻炼自己也帮助他人,逐渐构建服务生态,也治理服务。


目录
相关文章
|
27天前
|
JSON 安全 算法
|
1月前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
42 4
|
8天前
|
存储 运维 安全
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
通过以上措施,可以保证Spring Boot项目的配置管理在专业水准上,并且易于维护和管理,符合搜索引擎收录标准。
19 2
|
1月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
54 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
18天前
|
消息中间件 NoSQL Java
springboot整合常用中间件框架案例
该项目是Spring Boot集成整合案例,涵盖多种中间件的使用示例,每个案例项目使用最小依赖,便于直接应用到自己的项目中。包括MyBatis、Redis、MongoDB、MQ、ES等的整合示例。
73 1
|
28天前
|
XML Java 数据格式
提升效率!Spring Boot 开发中的常见失误轻松规避
本文深入探讨了在 Spring Boot 开发中常见的失误,包括不当使用注解、不良异常处理、低效日志记录等,提供了有效的规避策略,帮助开发者提升代码质量和系统性能,构建更健壮、高效的应用程序。
|
13天前
|
安全 Java 测试技术
Java开发必读,谈谈对Spring IOC与AOP的理解
Spring的IOC和AOP机制通过依赖注入和横切关注点的分离,大大提高了代码的模块化和可维护性。IOC使得对象的创建和管理变得灵活可控,降低了对象之间的耦合度;AOP则通过动态代理机制实现了横切关注点的集中管理,减少了重复代码。理解和掌握这两个核心概念,是高效使用Spring框架的关键。希望本文对你深入理解Spring的IOC和AOP有所帮助。
27 0
|
2月前
|
人工智能 开发框架 Java
重磅发布!AI 驱动的 Java 开发框架:Spring AI Alibaba
随着生成式 AI 的快速发展,基于 AI 开发框架构建 AI 应用的诉求迅速增长,涌现出了包括 LangChain、LlamaIndex 等开发框架,但大部分框架只提供了 Python 语言的实现。但这些开发框架对于国内习惯了 Spring 开发范式的 Java 开发者而言,并非十分友好和丝滑。因此,我们基于 Spring AI 发布并快速演进 Spring AI Alibaba,通过提供一种方便的 API 抽象,帮助 Java 开发者简化 AI 应用的开发。同时,提供了完整的开源配套,包括可观测、网关、消息队列、配置中心等。
2177 15
|
1月前
|
Java 数据库连接 Spring
【2021Spring编程实战笔记】Spring开发分享~(下)
【2021Spring编程实战笔记】Spring开发分享~(下)
26 1
|
1月前
|
缓存 NoSQL Java
Springboot自定义注解+aop实现redis自动清除缓存功能
通过上述步骤,我们不仅实现了一个高度灵活的缓存管理机制,还保证了代码的整洁与可维护性。自定义注解与AOP的结合,让缓存清除逻辑与业务逻辑分离,便于未来的扩展和修改。这种设计模式非常适合需要频繁更新缓存的应用场景,大大提高了开发效率和系统的响应速度。
61 2