一、@EnableCaching 源码解析

当要使用 @Cacheable 注解时需要引入 @EnableCaching 注解开启缓存功能。为什么呢?现在就来看看为什么要加入 @EnableCaching 注解才能开启缓存切面呢?源码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CachingConfigurationSelector.class)
public @interface EnableCaching {boolean proxyTargetClass() default false;AdviceMode mode() default AdviceMode.PROXY;int order() default Ordered.LOWEST_PRECEDENCE;
}

可以看出是通过 @Import 注解来导入一个类 CachingConfigurationSelector,猜测下,这个类肯定是一个入口类,也可以说是个触发类。注意此处 mode() 默认是 AdviceMode.PROXY

进入 CachingConfigurationSelector 类,源码如下:

public class CachingConfigurationSelector extends AdviceModeImportSelector<EnableCaching> {@Overridepublic String[] selectImports(AdviceMode adviceMode) {switch (adviceMode) {case PROXY:return getProxyImports();case ASPECTJ:return getAspectJImports();default:return null;}}private String[] getProxyImports() {List<String> result = new ArrayList<>(3);result.add(AutoProxyRegistrar.class.getName());result.add(ProxyCachingConfiguration.class.getName());if (jsr107Present && jcacheImplPresent) {result.add(PROXY_JCACHE_CONFIGURATION_CLASS);}return StringUtils.toStringArray(result);}
}

从 getProxyImports() 方法可知导入两个类:AutoProxyRegistrar 类、ProxyCachingConfiguration 类。那么接下来就重点分析这两个类是用来干啥的?

1、AutoProxyRegistrar

看到 XxxRegistrar 就可以确定要注册一个类。进入核心源码如下:

public class AutoProxyRegistrar implements ImportBeanDefinitionRegistrar {@Overridepublic void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);}@Nullablepublic static BeanDefinition registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, @Nullable Object source) {// 缓存和事物都是通过这个 InfrastructureAdvisorAutoProxyCreator 基础增强类来生成代理对象return registerOrEscalateApcAsRequired(InfrastructureAdvisorAutoProxyCreator.class, registry, source);}
}

很明显在这注册了一个 InfrastructureAdvisorAutoProxyCreator 类,这个类和事务切面入口类就是同一个。而这个类并不会做什么逻辑,所有的逻辑都在其父类 AbstractAutoProxyCreator 抽象类中,该类仅仅充当一个入口类。

AbstractAutoProxyCreator 类又是一个 BeanPostProcessor 接口的应用。所以关注这个接口的两个方法。这里只需要关注第二个方法 postProcessAfterInitialization(),源码如下:

 @Overridepublic Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {if (bean != null) {/** 获取缓存 key */Object cacheKey = getCacheKey(bean.getClass(), beanName);if (this.earlyProxyReferences.remove(cacheKey) != bean) {/** 是否有必要创建代理对象 */return wrapIfNecessary(bean, beanName, cacheKey);}}return bean;}

很明显在 AbstractAutoProxyCreator 抽象类中会调用 wrapIfNecessary() 方法去判断对当前 bean 是否需要创建代理对象。那么这里根据什么来判断呢?进入该方法,核心源码如下:

 protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);/** 合适的通知不为空 */if (specificInterceptors != DO_NOT_PROXY) {Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));return proxy;}return bean;}

在上面这段逻辑,很明显 getAdvicesAndAdvisorsForBean() 方法就是判断依据。如果有值就需要创建代理,反之不需要。那么重点关注该方法内部逻辑,核心源码如下:

 @Override@Nullableprotected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {/** tc_tag-99: 查找适合这个类的 advisors 切面通知 */List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);if (advisors.isEmpty()) {return DO_NOT_PROXY;}return advisors.toArray();}

继续进入 findEligibleAdvisors() 内部逻辑,核心源码如下:

 protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {List<Advisor> candidateAdvisors = findCandidateAdvisors();List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);extendAdvisors(eligibleAdvisors);if (!eligibleAdvisors.isEmpty()) {eligibleAdvisors = sortAdvisors(eligibleAdvisors);}return eligibleAdvisors;}

看到 findCandidateAdvisors() 方法,内部逻辑如下:

 protected List<Advisor> findCandidateAdvisors() {return this.advisorRetrievalHelper.findAdvisorBeans();}public List<Advisor> findAdvisorBeans() {String[] advisorNames = this.cachedAdvisorBeanNames;if (advisorNames == null) {advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, Advisor.class, true, false);this.cachedAdvisorBeanNames = advisorNames;}List<Advisor> advisors = new ArrayList<>();for (String name : advisorNames) {if (isEligibleBean(name)) {advisors.add(this.beanFactory.getBean(name, Advisor.class));}}return advisors;}

从上述代码中可以得知,Spring 通过调用 beanNamesForTypeIncludingAncestors(Advisor.class) 方法来获取所有 Spring 容器中实现 Advsior 接口的实现类。

那么现在先看到 ProxyCachingConfiguration 类,源码如下:

@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyCachingConfiguration extends AbstractCachingConfiguration {// cache_tag: 缓存方法增强器@Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)@Role(BeanDefinition.ROLE_INFRASTRUCTURE)public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor(CacheOperationSource cacheOperationSource, CacheInterceptor cacheInterceptor) {BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();// 缓存用来解析一些属性封装的对象 CacheOperationSourceadvisor.setCacheOperationSource(cacheOperationSource);// 缓存拦截器执行对象advisor.setAdvice(cacheInterceptor);if (this.enableCaching != null) {advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));}return advisor;}// 先省略一部分...
}

很明显 BeanFactoryCacheOperationSourceAdvisor 实现了 Advisor 接口。那么在上面调用 beanNamesForTypeIncludingAncestors(Advisor.class) 方法时就可以获取到该实例。该实例就会被提添加到候选集合 advisors 中返回。

okay,看完 findCandidateAdvisors() 方法, 再看到 findAdvisorsThatCanApply() 方法,前一个方法获取到了 Spring 容器中所有实现了 Advisor 接口的实现。然后再调用 findAdvisorsThatCanApply() 方法,去判断有哪些 Advisor 适用于当前 bean。进入 findAdvisorsThatCanApply() 内部逻辑,核心源码如下:

 public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {/** 没有切面,匹配个屁 */if (candidateAdvisors.isEmpty()) {return candidateAdvisors;}List<Advisor> eligibleAdvisors = new ArrayList<>();for (Advisor candidate : candidateAdvisors) {if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) {eligibleAdvisors.add(candidate);}}boolean hasIntroductions = !eligibleAdvisors.isEmpty();for (Advisor candidate : candidateAdvisors) {if (candidate instanceof IntroductionAdvisor) {// already processedcontinue;}// tc_tag-96: 开始 for 循环 candidateAdvisors 每个增强器,看是否能使用与这个 beanif (canApply(candidate, clazz, hasIntroductions)) {eligibleAdvisors.add(candidate);}}return eligibleAdvisors;}

继续进入 canApply() 核心逻辑如下:

 public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {/*** 通过 Pointcut 获取到 ClassFilter 类的匹配器* 然后匹配 targetClass 是否在 Pointcut 配置的包路径下面么?具体实现看 AspectJExpressionPointcut*/if (!pc.getClassFilter().matches(targetClass)) {return false;}/*** 通过 Pointcut 获取到 MethodMatcher 类的匹配器* 然后判断这个类下面的方法是否在 Pointcut 配置的包路径下面么?* 或者是这个方法上是否标注了 @Transactional、@Cacheable等注解呢?*/MethodMatcher methodMatcher = pc.getMethodMatcher();if (methodMatcher == MethodMatcher.TRUE) {// No need to iterate the methods if we're matching any method anyway...return true;}IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;if (methodMatcher instanceof IntroductionAwareMethodMatcher) {introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;}Set<Class<?>> classes = new LinkedHashSet<>();if (!Proxy.isProxyClass(targetClass)) {classes.add(ClassUtils.getUserClass(targetClass));}classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass));for (Class<?> clazz : classes) {/*** tc_tag1: 获取这个 targetClass 类下所有的方法,开始挨个遍历是否满 Pointcut 配置的包路径下面么?* 或者是这个方法上是否标注了 @Transactional、@Cacheable等注解呢?*/Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);for (Method method : methods) {if (introductionAwareMethodMatcher != null ?introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) :/*** tc_tag2: 注意对于 @Transactional 注解的 Pointcut 匹配还是比较复杂的,匹配逻辑在 TransactionAttributeSourcePointcut*/methodMatcher.matches(method, targetClass)) {return true;}}}return false;}

从上面源码中放眼过去,就能看到两个熟悉的老朋友:ClassFilter 类、MethodMatcher 类,ClassFilter 类是用来判断当前 bean 所在的 Class 上面是否有标注 @Caching@Cacheable@CachePut@CacheEvict 注解。MethodMatcher 类是用来判断当前 bean 的方法上是否有标注 @Caching@Cacheable@CachePut@CacheEvict 注解。

那么这两个过滤器对象是在哪里创建的呢?

回到 ProxyCachingConfiguration 类,源码如下:

@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyCachingConfiguration extends AbstractCachingConfiguration {// cache_tag: 缓存方法增强器@Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)@Role(BeanDefinition.ROLE_INFRASTRUCTURE)public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor(CacheOperationSource cacheOperationSource, CacheInterceptor cacheInterceptor) {BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();// 缓存用来解析一些属性封装的对象 CacheOperationSourceadvisor.setCacheOperationSource(cacheOperationSource);// 缓存拦截器执行对象advisor.setAdvice(cacheInterceptor);if (this.enableCaching != null) {advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));}return advisor;}
}

看到 BeanFactoryCacheOperationSourceAdvisor 类,我们知道一个 Advisor 必然是由 Advice 和 Pointcut 组成的。
而 Pointcut 肯定是由 ClassFilter 和 MethodMatcher 组成的。进入该类,源码如下:

public class BeanFactoryCacheOperationSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor {@Nullableprivate CacheOperationSource cacheOperationSource;private final CacheOperationSourcePointcut pointcut = new CacheOperationSourcePointcut() {@Nullableprotected CacheOperationSource getCacheOperationSource() {return cacheOperationSource;}};public void setCacheOperationSource(CacheOperationSource cacheOperationSource) {this.cacheOperationSource = cacheOperationSource;}public void setClassFilter(ClassFilter classFilter) {this.pointcut.setClassFilter(classFilter);}@Overridepublic Pointcut getPointcut() {return this.pointcut;}
}

关注到 CacheOperationSourcePointcut 类,进入源码如下:

abstract class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {protected CacheOperationSourcePointcut() {setClassFilter(new CacheOperationSourceClassFilter());}@Overridepublic boolean matches(Method method, Class<?> targetClass) {CacheOperationSource cas = getCacheOperationSource();return (cas != null && !CollectionUtils.isEmpty(cas.getCacheOperations(method, targetClass)));}@Nullableprotected abstract CacheOperationSource getCacheOperationSource();private class CacheOperationSourceClassFilter implements ClassFilter {@Overridepublic boolean matches(Class<?> clazz) {if (CacheManager.class.isAssignableFrom(clazz)) {return false;}CacheOperationSource cas = getCacheOperationSource();return (cas == null || cas.isCandidateClass(clazz));}}
}

从源码中可以看出 ClassFilter = CacheOperationSourceClassFilter,MethodMatcher = CacheOperationSourcePointcut。这里 ClassFilter 的 matches() 方法不对类进行过滤。这个类过滤放到了 MethodMatcher 的 matches() 方法中,和对方法的过滤集成在一起。

所以这里关注到 MethodMatcher#matches() 匹配方法,看看它是怎么进行匹配的。核心源码如下:

abstract class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {@Overridepublic boolean matches(Method method, Class<?> targetClass) {CacheOperationSource cas = getCacheOperationSource();return (cas != null && !CollectionUtils.isEmpty(cas.getCacheOperations(method, targetClass)));}
}

进入 getCacheOperations() 方法,核心源码如下:

 @Override@Nullablepublic Collection<CacheOperation> getCacheOperations(Method method, @Nullable Class<?> targetClass) {if (method.getDeclaringClass() == Object.class) {return null;}Object cacheKey = getCacheKey(method, targetClass);Collection<CacheOperation> cached = this.attributeCache.get(cacheKey);if (cached != null) {return (cached != NULL_CACHING_ATTRIBUTE ? cached : null);}else {// cache_tag: 计算缓存注解上面的配置的值,然后封装成 CacheOperation 缓存属性对象,基本和事物的一样// 注意每个缓存注解对应一种不同的解析处理Collection<CacheOperation> cacheOps = computeCacheOperations(method, targetClass);if (cacheOps != null) {if (logger.isTraceEnabled()) {logger.trace("Adding cacheable method '" + method.getName() + "' with attribute: " + cacheOps);}this.attributeCache.put(cacheKey, cacheOps);}else {this.attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE);}return cacheOps;}}

发现这段代码和 @Transactional 注解匹配一模一样,其实就是用的同一套逻辑,只不过注解不同而已。匹配过程是一模一样的,看到 computeCacheOperations() 方法,核心源码如下:

 @Nullableprivate Collection<CacheOperation> computeCacheOperations(Method method, @Nullable Class<?> targetClass) {if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {return null;}Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);Collection<CacheOperation> opDef = findCacheOperations(specificMethod);if (opDef != null) {return opDef;}opDef = findCacheOperations(specificMethod.getDeclaringClass());if (opDef != null && ClassUtils.isUserLevelMethod(method)) {return opDef;}if (specificMethod != method) {opDef = findCacheOperations(method);if (opDef != null) {return opDef;}// Last fallback is the class of the original method.opDef = findCacheOperations(method.getDeclaringClass());if (opDef != null && ClassUtils.isUserLevelMethod(method)) {return opDef;}}return null;}@Nullableprivate Collection<CacheOperation> parseCacheAnnotations(DefaultCacheConfig cachingConfig, AnnotatedElement ae, boolean localOnly) {Collection<? extends Annotation> anns = (localOnly ?AnnotatedElementUtils.getAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS) :AnnotatedElementUtils.findAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS));if (anns.isEmpty()) {return null;}// cache_tag: 熟悉不能再熟悉的缓存注解 Cacheable/CacheEvict/CachePut/Caching// 注意每一种类型的注解解析是不太一样的哦,具体看 parseCacheableAnnotation() 解析方法final Collection<CacheOperation> ops = new ArrayList<>(1);anns.stream().filter(ann -> ann instanceof Cacheable).forEach(ann -> ops.add(parseCacheableAnnotation(ae, cachingConfig, (Cacheable) ann)));anns.stream().filter(ann -> ann instanceof CacheEvict).forEach(ann -> ops.add(parseEvictAnnotation(ae, cachingConfig, (CacheEvict) ann)));anns.stream().filter(ann -> ann instanceof CachePut).forEach(ann -> ops.add(parsePutAnnotation(ae, cachingConfig, (CachePut) ann)));anns.stream().filter(ann -> ann instanceof Caching).forEach(ann -> parseCachingAnnotation(ae, cachingConfig, (Caching) ann, ops));return ops;}

从上面源码可以看到匹配逻辑,会去匹配当前这个 bean 的方法上是否有 @Cacheable 等注解。方法没找到,就找当前 bean 所在的类上,在没找到就找接口方法,在没找到,继续找接口类上,还灭找到直接返回 null。这个就是他的 MethodMatcher#matches() 匹配过程。

所以 canApply() 方法调用的底层就是调用 MethodMatcher 去进行匹配。findAdvisorsThatCanApply() 方法调用的就是 canApply(),就是这样一个匹配过程。如果能匹配成功,表示 Advisor 可以用于加强当前 bean。那么就需要去调用 createProxy() 方法创建代理对象。让代理对象去调用 Advisor 里面的增强逻辑,执行完之后再调用目标方法。返回到上层调用 getAdvicesAndAdvisorsForBean(),源码如下:

 @Override@Nullableprotected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {/** tc_tag-99: 查找适合这个类的 advisors 切面通知 */List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);if (advisors.isEmpty()) {return DO_NOT_PROXY;}return advisors.toArray();}

返回到最上层调用 wrapIfNecessary() ,源码如下:

 protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);/** 合适的通知不为空 */if (specificInterceptors != DO_NOT_PROXY) {Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));return proxy;}return bean;}

最后调用 createProxy() 创建代理对象。至此在 Spring 启动时创建代理对象环节完成。接下来要分析调用流程。

2、ProxyCachingConfiguration

对于该类就是提供缓存需要的支持类。比如 Advisor,通过 @Bean 方式提前注册到 Spring 容器中。源码如下:

@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyCachingConfiguration extends AbstractCachingConfiguration {// cache_tag: 缓存方法增强器@Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)@Role(BeanDefinition.ROLE_INFRASTRUCTURE)public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor(CacheOperationSource cacheOperationSource, CacheInterceptor cacheInterceptor) {BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();// 缓存用来解析一些属性封装的对象 CacheOperationSourceadvisor.setCacheOperationSource(cacheOperationSource);// 缓存拦截器执行对象advisor.setAdvice(cacheInterceptor);if (this.enableCaching != null) {advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));}return advisor;}// cache_tag: Cache 注解解析器@Bean@Role(BeanDefinition.ROLE_INFRASTRUCTURE)public CacheOperationSource cacheOperationSource() {return new AnnotationCacheOperationSource();}// cache_tag: 缓存拦截器执行器@Bean@Role(BeanDefinition.ROLE_INFRASTRUCTURE)public CacheInterceptor cacheInterceptor(CacheOperationSource cacheOperationSource) {CacheInterceptor interceptor = new CacheInterceptor();interceptor.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager);interceptor.setCacheOperationSource(cacheOperationSource);return interceptor;}}

这里只需要关注 Advisor。而 CacheOperationSource 对象只是对 @Cacheable 等注解解析之后的属性封装而已。

Advisor = Advice + Pointcut

Advisor = BeanFactoryCacheOperationSourceAdvisor

Advice = CacheInterceptor

Pointcut = CacheOperationSourcePointcut

Pointcut = ClassFilter + MethodMatcher

ClassFilter = CacheOperationSourceClassFilter

MethodMatcher = CacheOperationSourcePointcut

调用流程请跳转到下篇 Spring 之 @Cacheable 源码解析(下)

最终你会发现 @EnableCaching 注解和 @EnableTransactionManagement 基本一模一样,只是换了个注解,把 @Transactional 换成了 @Cacheable 等注解。

Spring 之 @Cacheable 源码解析(上)相关推荐

  1. Spring 之 @Cacheable 源码解析(下)

    CacheInterceptor 缓存切面处理逻辑 接着上篇 Spring 之 @Cacheable 源码解析(上) 说起,代理对象已经创建成功,接着分析调用流程.那么应该从哪里入手呢?当然是去看 A ...

  2. spring aop 注入源码解析

    spring aop 注入源码解析 aop启动 AbstractApplicationContext.java @Overridepublic void refresh() throws BeansE ...

  3. spring aop 注入源码解析 1

    spring aop 注入源码解析 aop启动 AbstractApplicationContext.java @Overridepublic void refresh() throws BeansE ...

  4. Spring Cloud Gateway 源码解析(3) —— Predicate

    目录 RoutePredicateFactory GatewayPredicate AfterRoutePredicateFactory RoutePredicateHandlerMapping Fi ...

  5. spring 多线程 事务 源码解析(一)

    大家好,我是烤鸭: 今天分享的是spring 多线程事务源码分析. 环境: spring-jdbc 5.0.4.REALEASE 今天分享一下spring事务的方法,这一篇还没涉及到多线程. 简单说一 ...

  6. Spring Cloud Gateway 源码解析(1) —— 基础

    目录 Gateway初始化 启用Gateway GatewayClassPathWarningAutoConfiguration GatewayLoadBalancerClientAutoConfig ...

  7. api网关揭秘--spring cloud gateway源码解析

    要想了解spring cloud gateway的源码,要熟悉spring webflux,我的上篇文章介绍了spring webflux. 1.gateway 和zuul对比 I am the au ...

  8. Android之EventBus框架源码解析上(单例模式)

    转载请标明出处:[顾林海的博客] 个人开发的微信小程序,目前功能是书籍推荐,后续会完善一些新功能,希望大家多多支持! 前言 EventBus能够简化各组件间的通信,让我们的代码书写变得简单,能有效的分 ...

  9. Spring Security OAuth2源码解析(一)

    目录 引入 AuthorizationServerEndpointsConfiguration 属性 AuthorizationEndpoint OAuth2RequestFactory Defaul ...

最新文章

  1. 无法解析的外部符号 class boost::system::error_category const __cdecl boost::system::system_category(void)
  2. 题目1209:最小邮票数
  3. python读取word文档
  4. java 前端页面调用数据库_java如何生成json被前端调用
  5. 5.1 Android Basic QuickStart Layouts Linear Layout
  6. 课程设计---约瑟夫环
  7. dom4j-2.1.1 jaxen-1.1.6 读取xml数据源
  8. myecplise新建Maven项目Filter选什么,使用myeclipse建立maven项目
  9. 工作339:pc父组件通过props传值给子组件,如何避免子组件改变props的属性值报错问题
  10. No projects are found to import
  11. 指纹图像方向图matlab,matlab指纹方向场方向图程序
  12. flask高级编程-循环引用
  13. Keyboard Control
  14. 史上最便捷搭建 Zookeeper 的方法!
  15. Visual Studio Installer 一直提取文件0B不动怎么办:修改DNS教程
  16. 计算机系系徽设计说明,系徽设计大赛策划书
  17. 润乾报表设计器——预览报表问题解决
  18. duang,duang!!duang.java.mustReadTips
  19. VMware 日记一:基础的系统安装和基本配置解析
  20. 背包问题不同要求下的初始化

热门文章

  1. 美通企业周刊 | 生成式AI成为全球焦点;诺维信和科汉森股东批准合并;沈阳威斯汀酒店开业...
  2. 学习乐器的好处(1)
  3. 传统情感分类方法与深度学习的情感分类方法对比
  4. 【数据结构进阶】二叉平衡树
  5. 高通7x27a平台UART配置
  6. 微信也能设置主题了,盘他!
  7. Unity 1.Roll a Ball
  8. 【干货】百度自动化运维是怎么做的?
  9. 学习的 定义是什么?生物
  10. 仿网易云音乐网站(加入个人社区)