前言介绍
Spring Boot + 领域驱动设计使得微服务越来越火热,而随着微服务越来越多,服务的治理就显得尤为重要。
在我们的业务领域开发中,经常会有一些通用性功能搭建,比如;白名单、黑名单、限流、熔断等,为了更好的开发业务功能,我们需要将非业务功能的通用逻辑提取出来开发出通用组件,以便于业务系统使用。而不至于Copy来Copy去,让代码乱的得加薪才能修改的地步!
通常一个中间件开发会需要用到;自定义xml配置、自定义Annotation注解、动态代理、反射调用、字节码编程(javaassist、ASM等),以及一些动态注册服务中心和功能逻辑开发等。本案例会使用Spring Boot开发方式定义自己的starter。
原理简述
通过我们使用一个公用的starter的时候,只需要将相应的依赖添加的Maven的配置文件当中即可,免去了自己需要引用很多依赖类,并且SpringBoot会自动进行类的自动配置。而我们自己开发一个starter也需要做相应的处理;
- SpringBoot 在启动时会去依赖的starter包中寻找 resources/META-INF/spring.factories 文件,然后根据文件中配置的Jar包去扫描项目所依赖的Jar包,这类似于 Java 的 SPI 机制。
SPI 全称 Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。
- 根据 spring.factories配置加载AutoConfigure类。
- 根据 @Conditional注解的条件,进行自动配置并将Bean注入Spring Context 上下文当中。也可以使用@ImportAutoConfiguration({MyServiceAutoConfiguration.class}) 指定自动配置哪些类。
- 日常使用的Spring官方的Starter一般采取spring-boot-starter-{name} 的命名方式,如 spring-boot-starter-web 。而非官方的Starter,官方建议 artifactId 命名应遵循{name}-spring-boot-starter 的格式。 例如:door-spring-boot-starter 。
环境准备
- jdk 1.8.0
- Maven 3.x
- 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,多个逗号隔开
测试验证
- 启动工程(可以Debug调试);itstack-demo-springboot-helloworld
- 访问连接;
- 白名单用户:http://localhost:8080/api/queryUserInfo?userId=1001
java {"code":"0000","info":"success","name":"虫虫:1001","age":19,"address":"天津市南开区旮旯胡同100号"}
- 非名单用户:http://localhost:8080/api/queryUserInfo?userId=小团团
java {"code":"1111","info":"非白名单可访问用户拦截!","name":null,"age":null,"address":null}
- 服务度日志;
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是比较的趋势,我会不断的深挖以及开发一些服务组件。锻炼自己也帮助他人,逐渐构建服务生态,也治理服务。