Spring 之 @Cacheable 源码解析(上)
一、@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 源码解析(上)相关推荐
- Spring 之 @Cacheable 源码解析(下)
CacheInterceptor 缓存切面处理逻辑 接着上篇 Spring 之 @Cacheable 源码解析(上) 说起,代理对象已经创建成功,接着分析调用流程.那么应该从哪里入手呢?当然是去看 A ...
- spring aop 注入源码解析
spring aop 注入源码解析 aop启动 AbstractApplicationContext.java @Overridepublic void refresh() throws BeansE ...
- spring aop 注入源码解析 1
spring aop 注入源码解析 aop启动 AbstractApplicationContext.java @Overridepublic void refresh() throws BeansE ...
- Spring Cloud Gateway 源码解析(3) —— Predicate
目录 RoutePredicateFactory GatewayPredicate AfterRoutePredicateFactory RoutePredicateHandlerMapping Fi ...
- spring 多线程 事务 源码解析(一)
大家好,我是烤鸭: 今天分享的是spring 多线程事务源码分析. 环境: spring-jdbc 5.0.4.REALEASE 今天分享一下spring事务的方法,这一篇还没涉及到多线程. 简单说一 ...
- Spring Cloud Gateway 源码解析(1) —— 基础
目录 Gateway初始化 启用Gateway GatewayClassPathWarningAutoConfiguration GatewayLoadBalancerClientAutoConfig ...
- api网关揭秘--spring cloud gateway源码解析
要想了解spring cloud gateway的源码,要熟悉spring webflux,我的上篇文章介绍了spring webflux. 1.gateway 和zuul对比 I am the au ...
- Android之EventBus框架源码解析上(单例模式)
转载请标明出处:[顾林海的博客] 个人开发的微信小程序,目前功能是书籍推荐,后续会完善一些新功能,希望大家多多支持! 前言 EventBus能够简化各组件间的通信,让我们的代码书写变得简单,能有效的分 ...
- Spring Security OAuth2源码解析(一)
目录 引入 AuthorizationServerEndpointsConfiguration 属性 AuthorizationEndpoint OAuth2RequestFactory Defaul ...
最新文章
- 无法解析的外部符号 class boost::system::error_category const __cdecl boost::system::system_category(void)
- 题目1209:最小邮票数
- python读取word文档
- java 前端页面调用数据库_java如何生成json被前端调用
- 5.1 Android Basic QuickStart Layouts Linear Layout
- 课程设计---约瑟夫环
- dom4j-2.1.1 jaxen-1.1.6 读取xml数据源
- myecplise新建Maven项目Filter选什么,使用myeclipse建立maven项目
- 工作339:pc父组件通过props传值给子组件,如何避免子组件改变props的属性值报错问题
- No projects are found to import
- 指纹图像方向图matlab,matlab指纹方向场方向图程序
- flask高级编程-循环引用
- Keyboard Control
- 史上最便捷搭建 Zookeeper 的方法!
- Visual Studio Installer 一直提取文件0B不动怎么办:修改DNS教程
- 计算机系系徽设计说明,系徽设计大赛策划书
- 润乾报表设计器——预览报表问题解决
- duang,duang!!duang.java.mustReadTips
- VMware 日记一:基础的系统安装和基本配置解析
- 背包问题不同要求下的初始化