大兴网站建设服务公司,昆明网站开发的公司,上海360网站建设,个人如何做微商城网站了解自动装配两个核心
Import注解的作用
Import说Spring框架经常会看到的注解#xff0c;它有以下几个作用:
导入Configuration类下所有的bean方法中创建的bean。导入import指定的bean#xff0c;例如Import(AService.class)#xff0c;就会生成AService的bean#xff0…了解自动装配两个核心
Import注解的作用
Import说Spring框架经常会看到的注解它有以下几个作用:
导入Configuration类下所有的bean方法中创建的bean。导入import指定的bean例如Import(AService.class)就会生成AService的bean并将其导入到Spring容器中。结合ImportSelector接口类导如指定类。(后文会展开介绍)
ImportSelector详解
ImportSelector接口则是前者的辅助者如果我们希望可以选择性的导入一些类我们就可以继承ImportSelector接口编写一个ImportSelector类告知容器需要导入的类。就以Spring Boot为例它有个EnableAutoConfiguration注解其工作原理就是基于内部的Import({AutoConfigurationImportSelector.class})注解将AutoConfigurationImportSelector导入容器中Spring就会调用其selectImports方法获取需要导入的类并将这些类导入容器中。
Overridepublic String[] selectImports(AnnotationMetadata annotationMetadata) {if (!isEnabled(annotationMetadata)) {return NO_IMPORTS;}AutoConfigurationEntry autoConfigurationEntry getAutoConfigurationEntry(annotationMetadata);//返回需要导入的类的字符串数组return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());}使用示例
可能上文的原理对没有接触源码的读者比较模糊所以我们不妨写一个demo来了解一下这个注解。我们现在有一个需求希望通过import注解按需将Student类或者User类导入容器中。首先我们看看user类代码没有任何实现代码示例如下:
public class User {
}
Student 类代码同理没有任何实现仅仅做测试使用
public class Student {
}
完成测试类的创建之后我们就以用户类为例创建UserConfig 代码如下
Configuration
public class UserConfig {Beanpublic User getUser() {return new User();}
}
然后编写ImportSelector 首先类编写自己的导入逻辑可以看到笔者简单实现了一个selectImports方法返回UserConfig的类路径。
public class CustomImportSelector implements ImportSelector {private static Logger logger LoggerFactory.getLogger(CustomImportSelector.class);/*** importingClassMetadata:被修饰的类注解信息*/Overridepublic String[] selectImports(AnnotationMetadata importingClassMetadata) {logger.info(获取到的注解类型:{},importingClassMetadata.getAnnotationTypes().toArray());// 如果被CustomImportSelector导入的组件是类那么我们就实例化UserConfigif (!importingClassMetadata.isInterface()) {return new String[] { com.example.UserConfig };}// 此处不要返回nullreturn new String[] { com.example.StudentConfig };}
}完成这些步骤我们就要来到最关键的一步了在Spring Boot启动类中使用Import导入CustomImportSelector
SpringBootApplication
Configuration
Import(CustomImportSelector.class)
public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}}
为了测试我们编写这样一个controller看看bean是否会导入到容器中
RestController
public class MyController {private static Logger logger LoggerFactory.getLogger(MyController.class);Autowiredprivate User user;RequestMapping(hello)public String hello() {logger.info(user:{}, user);return hello;}
}
结果测试我们发现user不为空说明CustomImportSelector确实将UserConfig导入到容器中并将User导入到容器中了。
从源码角度了解ImportSelector工作原理
关于源码分析其实也很好做感兴趣的读者可以直接在CustomImportSelector打个断点就能知道工作原理了: 断点之后我们不妨用以终为始的方式了解一下过程首先入口是AbstractApplicationContext的refresh()方法它会调用一个invokeBeanFactoryPostProcessors(beanFactory);进行bean工厂后置操作
Overridepublic void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) {.........invokeBeanFactoryPostProcessors(beanFactory);........}
}步入代码可以看到容器会不断遍历各个postProcessor 即容器后置处理器然后执行他们的逻辑
for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {.....//执行各个postProcessor 的逻辑invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
}重点来了遍历过程中得到一个ConfigurationClassPostProcessor这个类就会得到我们的CustomImportSelector然后执行selectImports获取需要导入的类信息最终会生成一个SetConfigurationClass configClasses new LinkedHashSet(parser.getConfigurationClasses());
如下图所示可以看到configClasses就包含UserConfig 总结一下核心流程的时序图 完成上述步骤后ConfigurationClassPostProcessor就会通过这个set集合执行loadBeanDefinitions方法将需要的bean导入到容器中进行后续IOC操作。 上图代码如下所示
//configClasses 中就包含了UserConfig类
SetConfigurationClass configClasses new LinkedHashSet(parser.getConfigurationClasses());configClasses.removeAll(alreadyParsed);//执行 loadBeanDefinitions this.reader.loadBeanDefinitions(configClasses);Spring Boot自动装配原理(重点)
了解了import原理后我们了解Spring Boot自动装配原理也很简单了我们不妨看看Spring Boot的SpringBootApplication这个注解中包含一个EnableAutoConfiguration注解我们不妨点入看看可以看到它包含一个Import(AutoConfigurationImportSelector.class)注解从名字上我们可以知晓这是一个ImportSelector的实现类。
所以我们不妨看看它的selectImports逻辑可以看到它会通过getAutoConfigurationEntry方法获取需要装配的类然后通过StringUtils.toStringArray切割返回。所以我们不妨看看getAutoConfigurationEntry
Overridepublic String[] selectImports(AnnotationMetadata annotationMetadata) {if (!isEnabled(annotationMetadata)) {return NO_IMPORTS;}AutoConfigurationEntry autoConfigurationEntry getAutoConfigurationEntry(annotationMetadata);return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());}查看getAutoConfigurationEntry方法我们可以看到它通过getCandidateConfigurations获取各个xxxxAutoConfigure并返回结果
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {if (!isEnabled(annotationMetadata)) {return EMPTY_ENTRY;}AnnotationAttributes attributes getAttributes(annotationMetadata);//获取所有xxxxAutoConfigureListString configurations getCandidateConfigurations(annotationMetadata, attributes);//移除不需要的configurations removeDuplicates(configurations);SetString exclusions getExclusions(annotationMetadata, attributes);checkExcludedClasses(configurations, exclusions);configurations.removeAll(exclusions);configurations getConfigurationClassFilter().filter(configurations);fireAutoConfigurationImportEvents(configurations, exclusions);//返回结果return new AutoConfigurationEntry(configurations, exclusions);}而getCandidateConfigurations实际上是会通过一个loadSpringFactories方法如下所示遍历获取所有含有META-INF/spring.factories的jar包
private static MapString, ListString loadSpringFactories(ClassLoader classLoader) {MapString, ListString result (Map)cache.get(classLoader);if (result ! null) {return result;} else {HashMap result new HashMap();try {//解析这个配置文件获取所有配置类然后返回Enumeration urls classLoader.getResources(META-INF/spring.factories);.....return result;} catch (IOException var14) {throw new IllegalArgumentException(Unable to load factories from location [META-INF/spring.factories], var14);}}}
最终结果过滤解析回到我们上文说的beanDefinitionMap中最终通过IOC完成自动装配。
实践1-手写Spring Boot Starter中间件
了解自动装配我们不妨自己写一个中间件实践一下现在需求如下我们希望某些类的接口只有某几个用户可以访问所以我们希望编写一个中间件判断请求用户是什么身份如果没有权限则直接返回报错。
首先我们编写一个注解DoDoor 用key记录传入的用户idreturnJson返回没有权限的响应结果
Retention(RetentionPolicy.RUNTIME)
Target(ElementType.METHOD)
public interface DoDoor {String key() default ;String returnJson() default ;}然后在编写StarterServiceProperties ,使用ConfigurationProperties将itstack.door前缀的值和当前类userStr绑定。
/*** 通过itstack.door前缀的配置获取userStr信息*/
ConfigurationProperties(itstack.door)
public class StarterServiceProperties {private String userStr;public String getUserStr() {return userStr;}public void setUserStr(String userStr) {this.userStr userStr;}}
完成后在编写StarterService 这个类会将userStr切割成数组例如我们传111,222最终就会得到[111,222]
public class StarterService {private String userStr;public StarterService(String userStr) {this.userStr userStr;}public String[] split(String separatorChar) {return StringUtils.split(this.userStr, separatorChar);}}这些佐料写完之后我们就可以编写一个AOP类了可以看到这个AOP做的是很简单就是拦截带有DoDoor的请求将注解key配置的值和我们的userStr数组比对若包含则放行反之拦截。
Aspect
Component
public class DoJoinPoint {private Logger logger LoggerFactory.getLogger(DoJoinPoint.class);Autowiredprivate StarterService starterService;Pointcut(annotation(org.itstack.door.annotation.DoDoor))public void aopPoint() {}Around(aopPoint())public Object doRouter(ProceedingJoinPoint jp) throws Throwable {//获取内容Method method getMethod(jp);DoDoor door method.getAnnotation(DoDoor.class);//获取字段值String keyValue getFiledValue(door.key(), jp.getArgs());logger.info(itstack door handler method{} value{}, method.getName(), keyValue);if (null keyValue || .equals(keyValue)) return jp.proceed();//配置内容String[] split starterService.split(,);//白名单过滤for (String str : split) {if (keyValue.equals(str)) {return jp.proceed();}}//拦截return returnObject(door, method);}private Method getMethod(JoinPoint jp) throws NoSuchMethodException {Signature sig jp.getSignature();MethodSignature methodSignature (MethodSignature) sig;return getClass(jp).getMethod(methodSignature.getName(), methodSignature.getParameterTypes());}private Class? extends Object getClass(JoinPoint jp) throws NoSuchMethodException {return jp.getTarget().getClass();}//返回对象private Object returnObject(DoDoor doGate, Method method) throws IllegalAccessException, InstantiationException {Class? returnType method.getReturnType();String returnJson doGate.returnJson();if (.equals(returnJson)) {return returnType.newInstance();}return JSON.parseObject(returnJson, returnType);}//获取属性值private String getFiledValue(String filed, Object[] args) {String filedValue null;for (Object arg : args) {try {if (null filedValue || .equals(filedValue)) {filedValue BeanUtils.getProperty(arg, filed);} else {break;}} catch (Exception e) {if (args.length 1) {return args[0].toString();}}}return filedValue;}}编写我们的AutoConfigure ,根据条件决定上述的类是否导入
Configuration
ConditionalOnClass(StarterService.class)
EnableConfigurationProperties(StarterServiceProperties.class)
public class StarterAutoConfigure {Autowiredprivate StarterServiceProperties properties;BeanConditionalOnMissingBeanConditionalOnProperty(prefix itstack.door, value enabled, havingValue true)StarterService starterService() {return new StarterService(properties.getUserStr());}BeanConditionalOnMissingBeanConditionalOnProperty(prefix itstack.door, value enabled, havingValue true)DoJoinPoint doJoinPoint() {return new DoJoinPoint();}}完成后编写一个spring.factories导入这个AutoConfigure
org.springframework.boot.autoconfigure.EnableAutoConfigurationorg.itstack.door.config.StarterAutoConfigure修改一下pom本地打个包
dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-aop/artifactId
/dependencyplugingroupIdorg.apache.maven.plugins/groupIdartifactIdmaven-jar-plugin/artifactIdversion2.3.2/versionconfigurationarchiveaddMavenDescriptorfalse/addMavenDescriptorindextrue/indexmanifestaddDefaultSpecificationEntriestrue/addDefaultSpecificationEntriesaddDefaultImplementationEntriestrue/addDefaultImplementationEntries/manifestmanifestEntriesImplementation-Build${maven.build.timestamp}/Implementation-Build/manifestEntries/archive/configuration
/plugin
在其他应用中导入
dependencygroupIdorg.itatack.demo/groupIdartifactIddoor-spring-boot-starter/artifactIdversion1.0.1-SNAPSHOT/version/dependency编写配置
server:port: 9887spring:application:name: demo# 自定义中间件配置
itstack:door:enabled: trueuserStr: 1001,aaaa,ccc #白名单用户ID多个逗号隔开然后在导入这个中间件的应用中编写一个方法测试DoDoor
RestController
public class HelloWorldController {Autowiredprivate ApplicationContext applicationContext;DoDoor(key userId, returnJson {\code\:\1111\,\info\:\非白名单可访问用户拦截\})RequestMapping(path /user, method RequestMethod.GET)public Map queryUserInfo(RequestParam String userId) {MapString, DoJoinPoint beansOfType applicationContext.getBeansOfType(DoJoinPoint.class);Map resultMap new HashMap();resultMap.put(虫虫: userId, 天津市南开区旮旯胡同100号);return resultMap;}}
测试结果
C:\Users\xxxxcurl http://localhost:9887/user?userId1001132
{code:1111,info:非白名单可访问用户拦截}
C:\Users\xxxcurl http://localhost:9887/user?userId1111
{code:1111,info:非白名单可访问用户拦截}
C:\Users\xxcurl http://localhost:9887/user?userId1001
{虫虫:1001:天津市南开区旮旯胡同100号}源码是借鉴小傅哥的感兴趣的读者可以参考:
Spring Boot 中间件开发(一)《服务治理中间件之统一白名单验证》
实践2-通用日志组件
需求介绍
微服务项目中基于日志排查问题是非常重要的手段而日志属于非功能范畴的一个职责所以我们希望将日志打印和功能解耦。AOP就是非常不错的手段但是在每个服务中都编写一个切面显然是非常不可取的。 所以我们希望通过某种手段会编写一个通用日志打印工具只需一个注解即可实现对方法的请求响应进行日志打印。 所以我们这个例子仍然是利用自动装配原理编写一个通用日志组件。
实现步骤
创建日志插件模块cloud-component-logging-starter并引入我们需要的依赖如下所示因为笔者要对spring-web应用进行拦截所以用到的starter-web和aop模块以及为了打印响应结果笔者也用到hutool完整的依赖配置如下所示: dependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-aop/artifactId/dependencydependencygroupIdcn.hutool/groupIdartifactIdhutool-all/artifactId/dependency/dependencies编写日志注解,如下所示该注解的value用于记录当前方法要执行的操作例如某方法上SysLog(获取用户信息)当我们的aop拦截到之后就基于该注解的value打印该方法的功能。
Target(ElementType.METHOD)
Retention(RetentionPolicy.RUNTIME)
public interface SysLog {/*** 记录方法要执行的操作** return*/String value();
}
编写环绕切面逻辑代码如下所示逻辑非常简单拦截到了切面后若报错则打印报错的逻辑反之打印正常请求响应结果。
Aspect
public class SysLogAspect {private static Logger logger LoggerFactory.getLogger(SysLogAspect.class);Pointcut(annotation(com.zsy.annotation.SysLog))public void logPointCut() {}Around(logPointCut())public Object around(ProceedingJoinPoint joinPoint) throws Throwable {MethodSignature signature (MethodSignature) joinPoint.getSignature();Method method signature.getMethod();//类名String className joinPoint.getTarget().getClass().getName();//方法名String methodName signature.getName();SysLog syslog method.getAnnotation(SysLog.class);//获取当前方法进行的操作String operator syslog.value();long beginTime System.currentTimeMillis();Object returnValue null;Exception ex null;try {returnValue joinPoint.proceed();return returnValue;} catch (Exception e) {ex e;throw e;} finally {long cost System.currentTimeMillis() - beginTime;if (ex ! null) {logger.error(业务请求:[类名: {}][执行方法: {}][执行操作: {}][耗时: {}ms][请求参数: {}][发生异常],className, methodName, operator, joinPoint.getArgs(), ex);} else {logger.info(业务请求:[类名: {}][执行方法: {}][执行操作: {}][耗时: {}ms][请求参数: {}][响应结果: {}],className, methodName, operator, cost, joinPoint.getArgs(), JSONUtil.toJsonStr(returnValue));}}}
}编写配置类
Configuration
public class SysLogAutoConfigure {Beanpublic SysLogAspect getSysLogAspect() {return new SysLogAspect();}
}
新建spring.factories告知要导入Spring容器的类内容如下
org.springframework.boot.autoconfigure.EnableAutoConfiguration\
com.zsy.config.SysLogAutoConfigure其他服务引入进行测试以笔者为例方法如下
SysLog(获取用户信息)GetMapping(getByCode/{accountCode})public ResultDataAccountDTO getByCode(PathVariable(value accountCode) String accountCode) {log.info(远程调用feign接口请求参数:{}, accountCode);return accountFeign.getByCode(accountCode);}请求之后输出结果如下
2023-02-16 00:08:08,085 INFO SysLogAspect:58 - 业务请求:[类名: com.zsy.order.controller.OrderController][执行方法: getByCode][执行操作: 获取用户信息][耗时: 892ms][请求参数: [zsy]][响应结果: {data:{accountCode:zsy,amount:10000,accountName:zsy,id:1},message:操作成功,success:true,status:100,timestamp:1676477287856}]参考文献
SpringBoot 自动装配原理
Import、ImportSelector注解使用及源码分析
SpringBoot封装我们自己的Starter
Spring Boot 中间件开发(一)《服务治理中间件之统一白名单验证》
SpringCloud Alibaba微服务实战三十一 - 业务日志组件
Spring全解系列 - Import注解