我们今天来聊一下Spring的AOP,我们从AnnotationAwareAspectJAutoProxyCreator开始说起。他是一个InstantiationAwareBeanPostProcessor所以我们需要看一下他的生命周期方法postProcessBeforeInstantiation和postProcessAfterInitialization,这两个是有具体处理逻辑的。首先我们看一下postProcessBeforeInstantiation
@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException { Object cacheKey = getCacheKey(beanClass, beanName); if (beanName == null || !this.targetSourcedBeans.contains(beanName)) { if (this.advisedBeans.containsKey(cacheKey)) { return null;
} if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) { this.advisedBeans.put(cacheKey, Boolean.FALSE); return null;
}
} // Create proxy here if we have a custom TargetSource.
// Suppresses unnecessary default instantiation of the target bean:
// The TargetSource will handle target instances in a custom fashion.
if (beanName != null) {
TargetSource targetSource = getCustomTargetSource(beanClass, beanName); if (targetSource != null) { this.targetSourcedBeans.add(beanName); Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource); Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource); this.proxyTypes.put(cacheKey, proxy.getClass()); return proxy;
}
} return null;
}根据beanClass和beanName创建cacheKey.如果beanName为空,并且targetSourcedBeans里面不包含此beanName进入下面的流程,如果advisedBeans包含了这个cacheKey直接返回,另外就是判断他是否是一个基础设施类(isInfrastructureClass)和是否应该跳过(shouldSkip)。首先我们看下基础设施类的判断
protected boolean isInfrastructureClass(Class<?> beanClass) {
boolean retVal = Advice.class.isAssignableFrom(beanClass) ||
Advisor.class.isAssignableFrom(beanClass) ||
AopInfrastructureBean.class.isAssignableFrom(beanClass); if (retVal && logger.isTraceEnabled()) {
logger.trace("Did not attempt to auto-proxy infrastructure class [" + beanClass.getName() + "]");
} return retVal;
}如果beanClass是Advice、Advisor、AopInfrastructureBean是他们的中的一种,那么就是基础设施类。我们再看下shouldSkip
@Override
protected boolean shouldSkip(Class<?> beanClass, String beanName) { // TODO: Consider optimization by caching the list of the aspect names
List<Advisor> candidateAdvisors = findCandidateAdvisors(); //关注点1
for (Advisor advisor : candidateAdvisors) { if (advisor instanceof AspectJPointcutAdvisor) { if (((AbstractAspectJAdvice) advisor.getAdvice()).getAspectName().equals(beanName)) { return true;
}
}
} return super.shouldSkip(beanClass, beanName);
}里面有一个重要的方法,寻找候选的Advisors,这个方法的具体实现是org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors
@Override
protected List<Advisor> findCandidateAdvisors() { // Add all the Spring advisors found according to superclass rules.
List<Advisor> advisors = super.findCandidateAdvisors(); // Build Advisors for all AspectJ aspects in the bean factory.
advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors()); return advisors;
}这个方法返回的数据分为两部分,第一部分是super.findCandidateAdvisors(),第二部分是this.aspectJAdvisorsBuilder.buildAspectJAdvisors()。我们一个个的说,第一部分是获取spring容器中的Advisor类型的Bean的实例。
public List<Advisor> findAdvisorBeans() { // Determine list of advisor bean names, if not cached already.
String[] advisorNames = null; synchronized (this) {
advisorNames = this.cachedAdvisorBeanNames; if (advisorNames == null) { // Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let the auto-proxy creator apply to them!
advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( this.beanFactory, Advisor.class, true, false); this.cachedAdvisorBeanNames = advisorNames;
}
} if (advisorNames.length == 0) { return new LinkedList<Advisor>();
}
List<Advisor> advisors = new LinkedList<Advisor>(); for (String name : advisorNames) { if (isEligibleBean(name)) { if (this.beanFactory.isCurrentlyInCreation(name)) { if (logger.isDebugEnabled()) {
logger.debug("Skipping currently created advisor '" + name + "'");
}
} else { try {
advisors.add(this.beanFactory.getBean(name, Advisor.class));
} catch (BeanCreationException ex) {
Throwable rootCause = ex.getMostSpecificCause(); if (rootCause instanceof BeanCurrentlyInCreationException) {
BeanCreationException bce = (BeanCreationException) rootCause; if (this.beanFactory.isCurrentlyInCreation(bce.getBeanName())) { if (logger.isDebugEnabled()) {
logger.debug("Skipping advisor '" + name + "' with dependency on currently created bean: " + ex.getMessage());
} // Ignore: indicates a reference back to the bean we're trying to advise.
// We want to find advisors other than the currently created bean itself.
continue;
}
} throw ex;
}
}
}
} return advisors;
}整体的流程是通过BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, Advisor.class, true, false)获取到所有的Advisor类型的Bean的名称,如果名称的列表为空,返回空的列表,如果不为空,遍历名称,得到对应的Bean,添加到列表中,返回列表,第一部分到此结束,我们说下第二部分,也是我们应该格外关注的this.aspectJAdvisorsBuilder.buildAspectJAdvisors()
public List<Advisor> buildAspectJAdvisors() {
List<String> aspectNames = null;
synchronized (this) {
aspectNames = this.aspectBeanNames; if (aspectNames == null) {
List<Advisor> advisors = new LinkedList<Advisor>();
aspectNames = new LinkedList<String>(); String[] beanNames =
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, Object.class, true, false); for (String beanName : beanNames) { if (!isEligibleBean(beanName)) { continue;
} // We must be careful not to instantiate beans eagerly as in this
// case they would be cached by the Spring container but would not
// have been weaved
Class<?> beanType = this.beanFactory.getType(beanName); if (beanType == null) { continue;
} if (this.advisorFactory.isAspect(beanType)) {
aspectNames.add(beanName);
AspectMetadata amd = new AspectMetadata(beanType, beanName); if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
MetadataAwareAspectInstanceFactory factory = new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory); if (this.beanFactory.isSingleton(beanName)) { this.advisorsCache.put(beanName, classAdvisors);
} else { this.aspectFactoryCache.put(beanName, factory);
}
advisors.addAll(classAdvisors);
} else { // Per target or per this.
if (this.beanFactory.isSingleton(beanName)) { throw new IllegalArgumentException("Bean with name '" + beanName + "' is a singleton, but aspect instantiation model is not singleton");
}
MetadataAwareAspectInstanceFactory factory = new PrototypeAspectInstanceFactory(this.beanFactory, beanName); this.aspectFactoryCache.put(beanName, factory);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
}
} this.aspectBeanNames = aspectNames; return advisors;
}
} if (aspectNames.isEmpty()) { return Collections.emptyList();
}
List<Advisor> advisors = new LinkedList<Advisor>(); for (String aspectName : aspectNames) {
List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName); if (cachedAdvisors != null) {
advisors.addAll(cachedAdvisors);
} else {
MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
} return advisors;
}思路差不多,通过BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, Object.class, true, false)获取到容器中的所有的Bean的名称,是个列表,遍历列表,根据每个Bean的名称,获取对应的Bean的类型,根据beanType判断是否是切面this.advisorFactory.isAspect(beanType)
@Override
public boolean isAspect(Class<?> clazz) { return (hasAspectAnnotation(clazz) && !compiledByAjc(clazz));
} private boolean hasAspectAnnotation(Class<?> clazz) { return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null);
} /**
* We need to detect this as "code-style" AspectJ aspects should not be
* interpreted by Spring AOP.
*/
private boolean compiledByAjc(Class<?> clazz) { // The AJTypeSystem goes to great lengths to provide a uniform appearance between code-style and
// annotation-style aspects. Therefore there is no 'clean' way to tell them apart. Here we rely on
// an implementation detail of the AspectJ compiler.
for (Field field : clazz.getDeclaredFields()) { if (field.getName().startsWith(AJC_MAGIC)) { return true;
}
} return false;
}判断的条件很简单,这个类必须有@Aspect这个注解,并且没有被AspectJ编译过,判断的条件就是这个类中所有的属性,不能是“ajc$”开头。通过这两个判断我们找到了切面类,将这个beanName放入到了aspectNames中缓存。随后根据beanType和beanName创建了AspectMetadata的实例,一般我们不是使用@Aspectj注解的value,所以amd.getAjType().getPerClause().getKind()一般是PerClauseKind.SINGLETON这段代码和本讲无关,所有没有深入讲解,有兴趣的同学可以自己查看。然后进入下面的流程,根据beanFactory和beanName创建了一个MetadataAwareAspectInstanceFactory的实例factory,通过List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory)获取Advisor列表。可以看一下org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory#getAdvisors的源码
@Override
public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) {
Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName();
validate(aspectClass); // We need to wrap the MetadataAwareAspectInstanceFactory with a decorator
// so that it will only instantiate once.
MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory = new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory);
List<Advisor> advisors = new LinkedList<Advisor>(); //关注点2
for (Method method : getAdvisorMethods(aspectClass)) {
Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName); if (advisor != null) {
advisors.add(advisor);
}
} // If it's a per target aspect, emit the dummy instantiating aspect.
if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory);
advisors.add(0, instantiationAdvisor);
} // Find introduction fields.
for (Field field : aspectClass.getDeclaredFields()) {
Advisor advisor = getDeclareParentsAdvisor(field); if (advisor != null) {
advisors.add(advisor);
}
} return advisors;
}看一下关注点2,根据aspectClass获取这个类中的AdvisorMethod
private List<Method> getAdvisorMethods(Class<?> aspectClass) { final List<Method> methods = new LinkedList<Method>();
ReflectionUtils.doWithMethods(aspectClass, new ReflectionUtils.MethodCallback() { @Override
public void doWith(Method method) throws IllegalArgumentException { // Exclude pointcuts
if (AnnotationUtils.getAnnotation(method, Pointcut.class) == null) {
methods.add(method);
}
}
});
Collections.sort(methods, METHOD_COMPARATOR); return methods;
}获取这个类上带有@Pointcut注解的方法,并且排序。通过Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName)将带有@Pointcut注解的方法变成Advisor类型的实例,放入advisors这个集合中。方法可以变成Advisor类型的实例,属性也可以,如果属性上有@DeclareParents注解,根据此注解生成DeclareParentsAdvisor类型的实例,添加到advisors这个集合中返回。OK,this.advisorFactory.getAdvisors(factory)这个方法解释的差不多了,随后在将beanName和查到的这些List<Advisor>放入到advisorsCache这个Map中缓存。遍历结束后,将查到的所有的advisors返回,这是我们shouldSkip的关键流程--查询Advisors。再继续,如果beanName不为空,并且getCustomTargetSource(beanClass, beanName)得到的targetSource不为空,就会添加这个beanName到targetSourcedBeans这个Set中缓存,接下来有一个非常重要的方法getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource)根据指定的条件获取Advisors,也就是拦截器。我们看下他是怎么获取这些拦截器的。
@Override protected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, TargetSource targetSource) { List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName); if (advisors.isEmpty()) { return DO_NOT_PROXY;
} return advisors.toArray();
} 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获取所有的Advisors,但是我们不知道这个BeanClass会用到哪些Advisors或者都用不到,就需要一个挑选的过程,这个挑选的过程就是findAdvisorsThatCanApply方法
protected List<Advisor> findAdvisorsThatCanApply( List<Advisor> candidateAdvisors, Class<?> beanClass, String beanName) {
ProxyCreationContext.setCurrentProxiedBeanName(beanName); try { return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass);
} finally {
ProxyCreationContext.setCurrentProxiedBeanName(null);
}
}
public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) { if (candidateAdvisors.isEmpty()) { return candidateAdvisors;
} List<Advisor> eligibleAdvisors = new LinkedList<Advisor>(); 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 processed
continue;
} if (canApply(candidate, clazz, hasIntroductions)) {
eligibleAdvisors.add(candidate);
}
} return eligibleAdvisors;
}整体来说,挑选的方法集中在了canApply,所做的操作就是根据@Pointcut注解中value定义的格式,来匹配这个Bean,看能不能匹配上,能匹配上就是canApply.然后返回针对这个BeanClass能匹配上的Advisor。OK,那么Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource)的结果就是找到了这个类对应应该执行的Advisor,最后就是将这个类和这些Advisor包装成一个动态代理对象返回,也就是Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource)
protected Object createProxy(
Class<?> beanClass, String beanName, Object[] specificInterceptors, TargetSource targetSource) { if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
}
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this); if (!proxyFactory.isProxyTargetClass()) { if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
} else {
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors); for (Advisor advisor : advisors) {
proxyFactory.addAdvisor(advisor);
}
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);
proxyFactory.setFrozen(this.freezeProxy); if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
} return proxyFactory.getProxy(getProxyClassLoader());
}创建一个proxyFactory,往其中添加必要的属性,其中注意的是addAdvisor,添加完善后,开始调用proxyFactory.getProxy得到对应的动态代理对象
#org.springframework.aop.framework.ProxyFactory#getProxy(java.lang.ClassLoader)public Object getProxy(ClassLoader classLoader) { return createAopProxy().getProxy(classLoader);
}#org.springframework.aop.framework.ProxyCreatorSupport#createAopProxyprotected final synchronized AopProxy createAopProxy() { if (!this.active) {
activate();
} return getAopProxyFactory().createAopProxy(this);
}
#org.springframework.aop.framework.DefaultAopProxyFactory#createAopProxy
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass(); if (targetClass == null) { throw new AopConfigException("TargetSource cannot determine target class: " + "Either an interface or a target is required for proxy creation.");
} if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { return new JdkDynamicAopProxy(config);
} return new ObjenesisCglibAopProxy(config);
} else { return new JdkDynamicAopProxy(config);
}
}我们先看下JDK的动态代理的getProxy
public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException {
Assert.notNull(config, "AdvisedSupport must not be null"); if (config.getAdvisors().length == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) { throw new AopConfigException("No advisors and no TargetSource specified");
} this.advised = config;
} @Override
public Object getProxy() { return getProxy(ClassUtils.getDefaultClassLoader());
} @Override
public Object getProxy(ClassLoader classLoader) { if (logger.isDebugEnabled()) {
logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());
}
Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised);
findDefinedEqualsAndHashCodeMethods(proxiedInterfaces); return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}再看下CGLIB动态代理的getProxy
public CglibAopProxy(AdvisedSupport config) throws AopConfigException {
Assert.notNull(config, "AdvisedSupport must not be null"); if (config.getAdvisors().length == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) { throw new AopConfigException("No advisors and no TargetSource specified");
} this.advised = config; this.advisedDispatcher = new AdvisedDispatcher(this.advised);
}
@Override
public Object getProxy() { return getProxy(null);
} @Override
public Object getProxy(ClassLoader classLoader) { if (logger.isDebugEnabled()) {
logger.debug("Creating CGLIB proxy: target source is " + this.advised.getTargetSource());
} try {
Class<?> rootClass = this.advised.getTargetClass();
Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy");
Class<?> proxySuperClass = rootClass; if (ClassUtils.isCglibProxyClass(rootClass)) {
proxySuperClass = rootClass.getSuperclass();
Class<?>[] additionalInterfaces = rootClass.getInterfaces(); for (Class<?> additionalInterface : additionalInterfaces) { this.advised.addInterface(additionalInterface);
}
} // Validate the class, writing log messages as necessary.
validateClassIfNecessary(proxySuperClass, classLoader); // Configure CGLIB Enhancer...
Enhancer enhancer = createEnhancer(); if (classLoader != null) {
enhancer.setClassLoader(classLoader); if (classLoader instanceof SmartClassLoader &&
((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {
enhancer.setUseCache(false);
}
}
enhancer.setSuperclass(proxySuperClass);
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setStrategy(new ClassLoaderAwareUndeclaredThrowableStrategy(classLoader));
Callback[] callbacks = getCallbacks(rootClass);
Class<?>[] types = new Class<?>[callbacks.length]; for (int x = 0; x < types.length; x++) {
types[x] = callbacks[x].getClass();
} // fixedInterceptorMap only populated at this point, after getCallbacks call above
enhancer.setCallbackFilter(new ProxyCallbackFilter( this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
enhancer.setCallbackTypes(types); // Generate the proxy class and create a proxy instance.
return createProxyClassAndInstance(enhancer, callbacks);
} catch (CodeGenerationException ex) { throw new AopConfigException("Could not generate CGLIB subclass of class [" + this.advised.getTargetClass() + "]: " + "Common causes of this problem include using a final class or a non-visible class",
ex);
} catch (IllegalArgumentException ex) { throw new AopConfigException("Could not generate CGLIB subclass of class [" + this.advised.getTargetClass() + "]: " + "Common causes of this problem include using a final class or a non-visible class",
ex);
} catch (Exception ex) { // TargetSource.getTarget() failed
throw new AopConfigException("Unexpected AOP exception", ex);
}
}
@Override
@SuppressWarnings("unchecked") protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
Class<?> proxyClass = enhancer.createClass();
Object proxyInstance = null; if (objenesis.isWorthTrying()) { try {
proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache());
} catch (Throwable ex) {
logger.debug("Unable to instantiate proxy using Objenesis, " + "falling back to regular proxy construction", ex);
}
} if (proxyInstance == null) { // Regular instantiation via default constructor...
try {
proxyInstance = (this.constructorArgs != null ?
proxyClass.getConstructor(this.constructorArgTypes).newInstance(this.constructorArgs) :
proxyClass.newInstance());
} catch (Throwable ex) { throw new AopConfigException("Unable to instantiate proxy using Objenesis, " + "and regular proxy instantiation via default constructor fails as well", ex);
}
}
((Factory) proxyInstance).setCallbacks(callbacks); return proxyInstance;
}到目前为止我们讲了第一个生命周期方法postProcessBeforeInstantiation下面我们再看下第二个postProcessAfterInitialization的关键方法
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { if (beanName != null && this.targetSourcedBeans.contains(beanName)) { return bean;
} if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { return bean;
} if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { this.advisedBeans.put(cacheKey, Boolean.FALSE); return bean;
} // Create proxy if we have advice.
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); if (specificInterceptors != DO_NOT_PROXY) { this.advisedBeans.put(cacheKey, Boolean.TRUE); Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); this.proxyTypes.put(cacheKey, proxy.getClass()); return proxy;
} this.advisedBeans.put(cacheKey, Boolean.FALSE); return bean;
}所做的事情基本上和我们上面讲解的声明周期方法是一致的,获取Advices(getAdvicesAndAdvisorsForBean),生成动态代理对象createProxy( bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)),今天我们就讲到这里,大家自己消化一下吧。
作者:数齐
链接:https://www.jianshu.com/p/f28e5b2df0a9
共同学习,写下你的评论
评论加载中...
作者其他优质文章