Spring AOP源码解析——AOP动态代理原理和实现方式

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

Spring介绍

Spring(http://spring.io/)是一个轻量级的Java 开发框架,同时也是轻量级的IoC和AOP的容器框架,主要是针对JavaBean的生命周期进行管理的轻量级容器,可以单独使用,也可以和Struts框架,MyBatis框架等组合使用。

 

AOP介绍

AOP是什么

AOP技术利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。AOP代表的是一个横向的关系,如果说“对象”是一个空心的圆柱体,其中封装的是对象的属性和行为;那么面向方面编程的方法,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息。而剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。

 

AOP相关概念

方面(Aspect):一个关注点的模块化,这个关注点实现可能另外横切多个对象。事务管理是一个很好的横切关注点例子。方面用Spring的Advisor或拦截器实现。

连接点(Joinpoint): 程序执行过程中明确的点,如方法的调用或特定的异常被抛出。

通知(Advice): 在特定的连接点,AOP框架执行的动作。各种类型的通知包括“around”、“before”和“throws”通知。通知类型将在下面讨论。许多AOP框架包括Spring都是以拦截器做通知模型,维护一个“围绕”连接点的拦截器链。Spring中定义了四个advice: BeforeAdvice, AfterAdvice, ThrowAdvice和DynamicIntroductionAdvice

切入点(Pointcut): 指定一个通知将被引发的一系列连接点的集合。AOP框架必须允许开发者指定切入点:例如,使用正则表达式。 Spring定义了Pointcut接口,用来组合MethodMatcher和ClassFilter,可以通过名字很清楚的理解, MethodMatcher是用来检查目标类的方法是否可以被应用此通知,而ClassFilter是用来检查Pointcut是否应该应用到目标类上

目标对象(Target Object): 包含连接点的对象。也被称作被通知或被代理对象。

AOP代理(AOP Proxy): AOP框架创建的对象,包含通知。 在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。

 

AOP原理

AOP 实现的关键就在于 AOP 框架自动创建的 AOP 代理,AOP 代理则可分为静态代理和动态代理两大类,其中静态代理是指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强;而动态代理则在运行时借助于 JDK 动态代理、CGLIB 等在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。Spring AOP则采用的是动态代理来实现。

在本节中,我们只分中JDK动态代理的实现方式。

 

源码解析

准备工作

首先定义一个Spring AOP的配置文件spring-aop.xml。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><aop:config><aop:aspect id="TestAspect" ref="aspect"><aop:pointcut id="pointcut" expression="execution(* org.study.spring.aop.*.*(..))"/><aop:before method="doBefore" pointcut-ref="pointcut"/></aop:aspect></aop:config><bean id="aspect" class="org.study.spring.aop.Aspect"/><bean id="test" class="org.study.spring.aop.Test"/></beans>

由于我们只分析JDK动态代理的实现方式,所以需要定义一个接口。

public interface ITest{public void doSomething();
}

目标对象实现上面定义的接口。

public class Test implements ITest {public void doSomething() {System.out.println("do something");}
}

定义Aspect,这里我们以前置通知为例。

public class Aspect {public void doBefore(JoinPoint jp) {System.out.println("do before");}
}

编写程序入口代码,可以直接打断点进行调试。

ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
Test bean = context.getBean("test", Test.class);
bean.doSomething();

开始解析

根据上节对bean创建和初始化过程的分析,我们来到AbstractAutowireCapableBeanFactory.java类的initializeBean方法。在这个方法里Spring会对创建完成的Bean进行初始化,我们重点关注applyBeanPostProcessorsAfterInitializaton这个方法。

	/*** Initialize the given bean instance, applying factory callbacks* as well as init methods and bean post processors.* <p>Called from {@link #createBean} for traditionally defined beans,* and from {@link #initializeBean} for existing bean instances.* @param beanName the bean name in the factory (for debugging purposes)* @param bean the new bean instance we may need to initialize* @param mbd the bean definition that the bean was created with* (can also be {@code null}, if given an existing bean instance)* @return the initialized bean instance (potentially wrapped)* @see BeanNameAware* @see BeanClassLoaderAware* @see BeanFactoryAware* @see #applyBeanPostProcessorsBeforeInitialization* @see #invokeInitMethods* @see #applyBeanPostProcessorsAfterInitialization*/protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {if (System.getSecurityManager() != null) {AccessController.doPrivileged(new PrivilegedAction<Object>() {@Overridepublic Object run() {invokeAwareMethods(beanName, bean);return null;}}, getAccessControlContext());}else {invokeAwareMethods(beanName, bean);}Object wrappedBean = bean;if (mbd == null || !mbd.isSynthetic()) {wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);}try {invokeInitMethods(beanName, wrappedBean, mbd);}catch (Throwable ex) {throw new BeanCreationException((mbd != null ? mbd.getResourceDescription() : null),beanName, "Invocation of init method failed", ex);}if (mbd == null || !mbd.isSynthetic()) {wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);}return wrappedBean;}

进入applyBeanPostProcessorsAfterInitializaton方法,继续往下。

	@Overridepublic Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)throws BeansException {Object result = existingBean;for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {result = beanProcessor.postProcessAfterInitialization(result, beanName);if (result == null) {return result;}}return result;}

接着进入AbstractAutoProxyCreator.java类的postProcessAfterInitialization方法中,继续往下。

	/*** Create a proxy with the configured interceptors if the bean is* identified as one to proxy by the subclass.* @see #getAdvicesAndAdvisorsForBean*/@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {if (bean != null) {Object cacheKey = getCacheKey(bean.getClass(), beanName);if (!this.earlyProxyReferences.contains(cacheKey)) {return wrapIfNecessary(bean, beanName, cacheKey);}}return bean;}

打开wrapIfNecessary方法,我们可以看到,Spring在这里先获取配置好的Advisor信息,然后调用createProxy方法为目标对象创建了代理,接着将创建的代理对象返回。

	/*** Wrap the given bean if necessary, i.e. if it is eligible for being proxied.* @param bean the raw bean instance* @param beanName the name of the bean* @param cacheKey the cache key for metadata access* @return a proxy wrapping the bean, or the raw bean instance as-is*/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;}

进入createProxy方法,Spring根据传入的advisor配置信息,初始化ProxyFactory然后获取并返回代理对象,我们直接看最后一行proxyFactory.getProxy(getProxyClassLoader())。

	/*** Create an AOP proxy for the given bean.* @param beanClass the class of the bean* @param beanName the name of the bean* @param specificInterceptors the set of interceptors that is* specific to this bean (may be empty, but not null)* @param targetSource the TargetSource for the proxy,* already pre-configured to access the bean* @return the AOP proxy for the bean* @see #buildAdvisors*/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.java类的getProxy方法,只有一行代码。我们分为两个部分来分析:

第一部分,调用createAopProxy方法初始化AopProxy。

第二部分,调用getProxy方法获取代理对象。

	/*** Create a new proxy according to the settings in this factory.* <p>Can be called repeatedly. Effect will vary if we've added* or removed interfaces. Can add and remove interceptors.* <p>Uses the given class loader (if necessary for proxy creation).* @param classLoader the class loader to create the proxy with* (or {@code null} for the low-level proxy facility's default)* @return the proxy object*/public Object getProxy(ClassLoader classLoader) {return createAopProxy().getProxy(classLoader);}

我们先来看看AopProxy是如何被初始化的。

初始化AopProxy

先进入ProxyCreatorSupport.java类的createAopProxy方法。这就是生成代理的入口,你会发现传入的参数是是this,其实传入的就是this的父类AdvisedSupport.java中的advisor等生成代理的核心参数。

	/*** Subclasses should call this to get a new AOP proxy. They should <b>not</b>* create an AOP proxy with {@code this} as an argument.*/protected final synchronized AopProxy createAopProxy() {if (!this.active) {activate();}return getAopProxyFactory().createAopProxy(this);}

打开DefaultAopProxyFactory.java类中的createAopProxy方法。Spring根据代理的目标对象是否实现了接口,来返回JdkDynamicAopProxy的动态代理或者CGLIB的代理,并且传入advisor核心参数(JdkDynamicAopProxy这个实现了InvocationHandler,要实现invoke的关键就是传入的advisor)。

	@Overridepublic 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);}}

接着进入JdkDynamicAopProxy.java类中的JdkDynamicAopProxy方法,将传入的AdvisedSupport赋值到advised里。

	/*** Construct a new JdkDynamicAopProxy for the given AOP configuration.* @param config the AOP configuration as AdvisedSupport object* @throws AopConfigException if the config is invalid. We try to throw an informative* exception in this case, rather than let a mysterious failure happen later.*/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;}

到这里,AopProxy已初始化完成。接下来我们来看,Spring是如何获取代理对象的。

获取代理对象

先进入getProxy方法,这里我们重点关注newProxyInstance这个方法。

	@Overridepublic 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, true);findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);}

接着进入Proxy.java类的newProxyInstance方法,这是java reflect包提供的原生创建代理类的方法。就是在这里,目标对象的代理对象完成了创建并返回。

    /*** Returns an instance of a proxy class for the specified interfaces* that dispatches method invocations to the specified invocation* handler.** <p>{@code Proxy.newProxyInstance} throws* {@code IllegalArgumentException} for the same reasons that* {@code Proxy.getProxyClass} does.** @param   loader the class loader to define the proxy class* @param   interfaces the list of interfaces for the proxy class*          to implement* @param   h the invocation handler to dispatch method invocations to* @return  a proxy instance with the specified invocation handler of a*          proxy class that is defined by the specified class loader*          and that implements the specified interfaces* @throws  IllegalArgumentException if any of the restrictions on the*          parameters that may be passed to {@code getProxyClass}*          are violated* @throws  SecurityException if a security manager, <em>s</em>, is present*          and any of the following conditions is met:*          <ul>*          <li> the given {@code loader} is {@code null} and*               the caller's class loader is not {@code null} and the*               invocation of {@link SecurityManager#checkPermission*               s.checkPermission} with*               {@code RuntimePermission("getClassLoader")} permission*               denies access;</li>*          <li> for each proxy interface, {@code intf},*               the caller's class loader is not the same as or an*               ancestor of the class loader for {@code intf} and*               invocation of {@link SecurityManager#checkPackageAccess*               s.checkPackageAccess()} denies access to {@code intf};</li>*          <li> any of the given proxy interfaces is non-public and the*               caller class is not in the same {@linkplain Package runtime package}*               as the non-public interface and the invocation of*               {@link SecurityManager#checkPermission s.checkPermission} with*               {@code ReflectPermission("newProxyInPackage.{package name}")}*               permission denies access.</li>*          </ul>* @throws  NullPointerException if the {@code interfaces} array*          argument or any of its elements are {@code null}, or*          if the invocation handler, {@code h}, is*          {@code null}*/@CallerSensitivepublic static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)throws IllegalArgumentException{Objects.requireNonNull(h);final Class<?>[] intfs = interfaces.clone();final SecurityManager sm = System.getSecurityManager();if (sm != null) {checkProxyAccess(Reflection.getCallerClass(), loader, intfs);}/** Look up or generate the designated proxy class.*/Class<?> cl = getProxyClass0(loader, intfs);/** Invoke its constructor with the designated invocation handler.*/try {if (sm != null) {checkNewProxyPermission(Reflection.getCallerClass(), cl);}final Constructor<?> cons = cl.getConstructor(constructorParams);final InvocationHandler ih = h;if (!Modifier.isPublic(cl.getModifiers())) {AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {cons.setAccessible(true);return null;}});}return cons.newInstance(new Object[]{h});} catch (IllegalAccessException|InstantiationException e) {throw new InternalError(e.toString(), e);} catch (InvocationTargetException e) {Throwable t = e.getCause();if (t instanceof RuntimeException) {throw (RuntimeException) t;} else {throw new InternalError(t.toString(), t);}} catch (NoSuchMethodException e) {throw new InternalError(e.toString(), e);}}

以上就完成了创建并获取代理对象的整个过程。

 

总结

通过这次源码分析,我们应该知道AOP动态代理的原理是什么,也知道Spring是如何根据目标对象去创建并获取代理对象的。其实,整个过程的本质就是Spring根据配置文件,利用反射和目标对象实现所的接口创建了代理对象。然后将代理对象返回,与原对象进行替换,从而实现了动态代理。如果还有不明白的地方,可以对照着Spring的源码自己动手理解一下,希望能对大家有所帮助。

转载于:https://my.oschina.net/zhaojia/blog/778455

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/283528.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用.NET构建简单的高性能Redis(三)

译者注该原文是Ayende Rahien大佬业余自己在使用C# 和 .NET构建一个简单、高性能兼容Redis协议的数据库的经历。首先这个"Redis"是非常简单的实现&#xff0c;但是他在优化这个简单"Redis"路程很有趣&#xff0c;也能给我们在从事性能优化工作时带来一些启…

海尔联手软银机器人,进军服务机器人领域

海尔此次将正式全面进入到服务机器人。 据悉&#xff0c;3月6日海尔公布了未来对于家用机器人的最新战略&#xff0c;同时与软银展开战略合作&#xff0c;将软银的人形机器人引入中国市场&#xff0c;正式进军服务机器人领域。 在本次发布会上&#xff0c;海尔与软银将联手从软…

.NET 7 中的 EndpointFilter

ASP.NET 7 中的 EndpointFilterIntro.NET 6 开始微软引入了 Minimal API&#xff0c;但是相比成熟的控制器模型&#xff0c;还是有很多的不足&#xff0c;.NET 7 针对于 Minimal API 也做了一些改进来让 Minimal API 功能更加丰富&#xff0c;其中 Filter 就是其中的一个更新从…

越来越火的图数据库到底能做什么?

作者 | 俞方桦 随着大数据时代的到来&#xff0c;传统的关系型数据库由于其在数据建模和存储方面的限制&#xff0c;变得越来越难以满足大量频繁变化的需求。关系型数据库&#xff0c;尽管其名称中有“关系”这个词&#xff0c;却并不擅长处理复杂关系的查询和分析。另外&…

piwik抓取用户交互行为

2019独角兽企业重金招聘Python工程师标准>>> https://github.com/matomo-org/matomo-sdk-ios/tree/version-3 http://piwik.org 首先下载demo&#xff0c;把文件拖到项目中去&#xff0c;在AppDelegate.m文件填写piwik服务器的URL和编号&#xff1b; 例如&#xff1…

k8s 读书笔记 - kubernetes 基本概念和术语(下)

DevOps前言上一篇文章 中&#xff0c;我们介绍了 k8s 中的 Master、Node、Pod、Label、RC & RS、Deployment、HPA & VPA、DaemonSet 这些资源对象信息&#xff0c;接下来我们继续介绍 k8s 中常用的资源对象。StatefulSet在 k8s 系统中&#xff0c;Pod 的管理对象 RC、D…

java数据类型后加三个点...

2019独角兽企业重金招聘Python工程师标准>>> 从Java5开始&#xff0c;Java语言对方法参数支持一种新写法&#xff0c;varargs&#xff08;可变长度参数列表&#xff09;&#xff0c;其语法就是类型后跟...&#xff0c;表示此处接受的参数为0到多个Object类型的对象&…

手把手教你用 Jenkins 自动部署 SpringBoot!

1. 什么是 CI/CD 1.1 CI&#xff08;Continuous Integration&#xff09; 1.2 CD&#xff08;Continuous Delivery/Continuous Deployment&#xff09; 2. 什么是 Jenkins 3. 准备工作 3.1 整体规划 3.2 准备代码 3.3 准备服务器 4. 搭建 Jenkins 5. 安装插件 6. 配置 …

bondat蠕虫传播与对抗

转载来自&#xff1a;http://www.mottoin.com/109730.html &#xff08;1&#xff09;可移动磁盘传播手段&#xff1a;隐藏U盘文件&#xff0c;创建快捷方式指向病毒bat文件。Bondat蠕虫主要通过可移动磁盘传播&#xff0c;并借助可移动磁盘中的文件隐蔽自身。Bondat蠕虫会检索…

vim 编译 Python 代码提示配置

2019独角兽企业重金招聘Python工程师标准>>> .vim 和.vimrc 拷贝到根目录 注意根目录下默认是没有.vim的&#xff0c;所以拷贝.vim 没问题&#xff0c;但是拷贝.vimrc 之前需要把原来的.vimrc备份 两个文件下载&#xff1a;http://pan.baidu.com/s/1eRRhakM 转载于:…

[转]Pinia与Vuex的对比:Pinia是Vuex的良好替代品吗?

文章目录 介绍设置 Pinia 设置Vuex 设置使用 Pinia使用Vuex使用社区和生态系统的力量学习曲线和文档GitHub 评分性能比较 Pinia 2 和 Vuex 4Vuex 和 Pinia 的优缺点何时使用Pinia&#xff0c;何时使用Vuex介绍 Pinia 是 Vue.js 的轻量级状态管理库&#xff0c;最近很受欢迎。它…

1.2开发文档简读,了解全貌.mp4

转载于:https://www.cnblogs.com/ZHONGZHENHUA/p/6910254.html

开源:一款开源的高颜值现代化桌面美化工具

背景在日常的工作或学习中&#xff0c;难免会有一些临时的文件夹&#xff0c;文件&#xff0c;应用&#xff0c;出现在你的桌面&#xff0c;但是呢你又不确定它是不是哪一天会突然用到&#xff0c;这样一天又一天&#xff0c;直至你的电脑桌面是一片狼藉&#xff0c;满屏的文件…

软件工程—团队作业1

软件工程—团队作业1 团队称号&#xff1a;Thanos &#xff08;灭霸&#xff0c;超叼的一个动漫人物&#xff09; 团队成员&#xff1a; 队长 成凯 1600802002 博客链接&#xff1a; http://www.cnblogs.com/ck03/ 党康 1600208004 博客链接&#xff1a; http://www.cnblogs…

k8s 读书笔记 - kubernetes 基本概念和术语(上)

k8s 资源控制系统k8s 中大部分概念如&#xff1a;Node、Pod、Replication Controller、RS、Deployment、Service 等都可以被看作一种资源对象&#xff0c;激活所有的资源对象都可以通过 k8s 提供 kubectl 工具&#xff08;或者 API 编程调用&#xff09;执行 CRUD 等操作并将其…

CentOs6.5下安装svn

1、检查是否已安装 rpm -qa subversion 1、1如果需要卸载旧版本&#xff08;如果想在一台机器安装不同svn&#xff0c;切记不要执行此步骤&#xff01;&#xff01;&#xff01;&#xff09; yum remove subversion 2、安装 yum install subversion 3、检查安装是否成功 svnser…

Android 升级到android studio 2.2项目死活run不起来

背景&#xff1a;升级到Android studio 2.2项目死活运行不起来 现象如下&#xff1a; run with --stacktrace --debug等等抛出的bug简直无法忍视 解决办法&#xff1a;把compileSdkVersion 改为23成功run起来了

【python】-- Django 中间件、缓存、信号

Django 中间件、缓存、信号 一、 Django 中间件 django 中的中间件&#xff08;middleware&#xff09;&#xff0c;在django中&#xff0c;中间件其实就是一个类&#xff0c;在请求到来和结束后&#xff0c;django会根据自己的规则在合适的时机执行中间件中相应的方法。 在d…

【温故知新】C#中 IEnumerable 与IQueryable

微信公众号&#xff1a;趣编程ACE关注可了解更多的.NET日常实战开发技巧&#xff0c;如需源码 后台回复 源码 即可;如果觉得对你有帮助&#xff0c;欢迎关注老生常谈 C#中 IEnumerable 与IQueryableIEnumerable 与 IQueryable 对于.Neter来说并不陌生&#xff0c;今天我就着重阐…

Scala基础 - _root_ package的作用

2019独角兽企业重金招聘Python工程师标准>>> 在Scala中引入类时支持相对路径&#xff0c;例如&#xff1a; import play.api.libs.json._ import play.api.libs.json.util.LazyHelper可以简写成&#xff1a; import play.api.libs.json._ import util.LazyHelper通常…