PS * 文中代码均为伪代码,本文基于spring 5.0 ,如有谬误,感谢指正!!!

一、大话AOP

1.AOP的概念

  • AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。

  • AOP技术利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

从概念上来说很清晰,仅对入门小白来说存在一些门槛。

总结来说,AOP 提供的功能是 减少重复代码,通过切面来实现一些重复的逻辑代码。

2.必要的准备工作

个人认为阅读spring源码前务必请读一下常用设计模式,比如AOP 中最重要的设计模式:代理模式。

什么是代理模式?

  • 代理模式为另一个对象提供一个替身或者占位符,以控制对这个对象的访问。也可使是视作代理对象让客户代码于实际对象解耦。使用代理模式创建代表对象,让代表对象控制对某对象的访问,被代理的对象可以是远程的对象(运行在不同JVM上,通过RMI实现)、创建开销大的对象(虚拟代理模式)或者需要安全控制的对象(保护代理模式)。

可以简单认为,代理模式把<实际需要访问的对象>封装到了,一个<与实际对象实现了相同接口的对象>中,这个用于封装实际对象的外层对象,称之为 <代理对象>。对<被代理对象>的访问,全部通过<代理对象>进行。

-- 助记定义
<实际需要访问的对象> 即 被代理对象。
<与实际对象实现了相同接口的对象> 即 代理对象。

3.大话AOP

那么AOP 具体是什么呢?

对没有接触过AOP的,人来说,你跟他讲切面、切点、织入,这是非常抽象的。

用最直白的话来说:AOP 实现的逻辑就是为原对象生成<代理对象>,处理<代理对象> 最终实现增强。[ JDK动态代理: InvocationHandler.invoke()、CGLIB代理: MethodInterceptor.intercept() ]

实现AOP的方式

  1. 动态代理
  • JDK 动态代理

  • CGLIB 代理

    动态代理,在运行时动态织入增强,所以会对运行性能造成损耗。

  1. 静态代理

    在虚拟机加载字节码文件的时候,将增强内容织入到方法的字节码中,它提供了更细粒度的控制,对性能影响较小。

二、动态AOP自定义标签

全局搜索 AOP 配置项很容易定位到如下代码:

registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser());

最终接口方法parse 定位到如下代码

public static void registerAspectJAnnotationAutoProxyCreatorIfNecessary(
ParserContext parserContext, Element sourceElement) { // 是否已经存在:自动代理创建器,已存在判断优先级,不存在创建默认类型
BeanDefinition beanDefinition = AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(
parserContext.getRegistry(), parserContext.extractSource(sourceElement));
// 处理 proxy-target-class 和 expose-proxy 属性
// proxy-target-class 决定使用的代理类型:JDK动态代理,应用场景更加多元,通过实现接口代理,确定是性能不够强悍;CGLIB代理,通过继承目标类实现拓展,操作低层字节码,性能强悍。
// expose-proxy : 目标对象内部的自我调用将无法实施切面中的增强,利用 expose-proxy 属性可以解决这个问题
useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement);
// 注册组件并通知,便于监听器做进一步的处理
// beanDefinition 代表的是 AnnotationAwareAspectJAutoProxyCreator的类定义
registerComponentIfNecessary(beanDefinition, parserContext);
}

上述代码主要实现如下功能:

  • 注册或升级 AnnotationAwareAspectJAutoProxyCreator 注解解析器

  • 处理 proxy-target-class 和 expose-proxy 属性

1、JDK动态代理

其代理对象必须实现某个接口,它通过在运行期间,创建接口的实现类来完成对目标对象的代理。

2、CGLIB 代理

它在运行期间生成的代理对象是针对目标类型拓展的子类。

目标对象使用动态代理方式总结:

  • 目标对象实现了接口,默认使用JDK代理;
  • 目标对象实现了接口可以强制使用CGLIB代理;
  • 目标对象没有实现接口,必须使用CGLIB代理。
	JDK 动态代理只能针对实现了接口的类生成代理。
CGLIB代理 为指定类生成一个子类,并覆盖其中的方法,需要被增强的类或方法不能定义为 final

三、创建AOP代理

AnnotationAwareAspectJAutoProxyCreator 类层次结构图

上一步实现了 AnnotationAwareAspectJAutoProxyCreator 的注册或升级,

它实现了 BeanPostProcessor 接口[实现该接口的后处理器,在Bean实例化时都会调用其postProcessAfterInitialization() 方法]

追踪代码可以发现,该方法由父类AbstractAutoProxyCreator 实现

追踪代码发现:

// 要么返回bean 要么返回被代理后的bean-proxy
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
// advisedBeans : 需要被增强的
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;
}

它主要包含的内容是:

1 . 获取增强方法 或者 增强器

2 . 对获取到的增强进行代理

本节剩下的内容都是在讲述他们

关注获取增强方法的 getAdvicesAndAdvisorsForBean 方法,由 父类 AbstractAdvisorAutoProxyCreator 实现

根据方法找到如下实现:


protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
// AnnotationAwareAspectJAutoProxyCreator 类覆盖了该方法
List<Advisor> candidateAdvisors = findCandidateAdvisors();// 获取所有的增强 (所有 拦截||切点???)
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);// 寻找适用于当前 Bean 的增强
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
return eligibleAdvisors;
}

1.获取所有增强器

public List<Advisor> findAdvisorBeans() {
// Determine list of advisor bean names, if not cached already.
String[] 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!
// 获取 beanFactory 中的所有 注册的 beanName && 声明为 @AspectJ
advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this.beanFactory, Advisor.class, true, false);
this.cachedAdvisorBeanNames = advisorNames;
}
if (advisorNames.length == 0) {
return new ArrayList<>();
} List<Advisor> advisors = new ArrayList<>();// 记录增强 ??
for (String name : advisorNames) {
if (isEligibleBean(name)) {// 模板方法模式思想-钩子函数 : 校验 该bean 是否合格????
if (this.beanFactory.isCurrentlyInCreation(name)) { // 新 创建 ??
if (logger.isDebugEnabled()) {
logger.debug("Skipping currently created advisor '" + name + "'");
}
}
else {
// 提取 增强并缓存
advisors.add(this.beanFactory.getBean(name, Advisor.class));
}
}
}
return advisors;
}

2.寻找匹配的增强器

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 processed
continue;
}
// 普通 bean 处理
if (canApply(candidate, clazz, hasIntroductions)) {
eligibleAdvisors.add(candidate);
}
}
return eligibleAdvisors;
}

3.创建代理

最后回到 wrapIfNecessary 方法,获取到增强方法后,将调用 createProxy 创建代理

protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable 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);// 拦截器封装为 Advisor
proxyFactory.addAdvisors(advisors);// 加入增强器
proxyFactory.setTargetSource(targetSource);// 设置要代理的类
customizeProxyFactory(proxyFactory);// 定制化内容,钩子函数 proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
} return proxyFactory.getProxy(getProxyClassLoader());// 获取代理
}

最终指向 createAopProxy().getProxy();

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

查看代码很容易看到,ObjenesisCglibAopProxy 和 JdkDynamicAopProxy 都实现了接口 AopProxy

四、创建AOP静态代理

1.自定义标签

全局搜索 AOP静态织入 配置项:load-time-weaver 可以发现如下的代码入口

registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());

LoadTimeWeaverBeanDefinitionParser类层次结构图

查看该解析类,发现 经由父类把 parse 方法 转为了 doParse 方法

    // 以标签为标志,进行相关  《处理类》 的注册
// parse 到 doParse的转化: parse() -> parseInternal() -> doParse()
// 解析(×) 注册工具类(√)
// 解析load-time-weaver 标签会产生一个 beanName为 loadTimeWerver 的 bean class=org.springframework.context.weaving.DefaultContextLoadTimeWeaver
@Override
protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); // 检查AspectJ 功能是否可用(load-time-weaver:-> aspectj-weaving<on off autodetect>) {default=autodetect 检查 META-INF/aop.xml 是否存在}
if (isAspectJWeavingEnabled(element.getAttribute(ASPECTJ_WEAVING_ATTRIBUTE), parserContext)) {// aspectj-weaving
if (!parserContext.getRegistry().containsBeanDefinition(ASPECTJ_WEAVING_ENABLER_BEAN_NAME)) {// org.springframework.context.config.internalAspectJWeavingEnabler
RootBeanDefinition def = new RootBeanDefinition(ASPECTJ_WEAVING_ENABLER_CLASS_NAME);// org.springframework.context.weaving.AspectJWeavingEnabler
parserContext.registerBeanComponent(
// org.springframework.context.config.internalAspectJWeavingEnabler
new BeanComponentDefinition(def, ASPECTJ_WEAVING_ENABLER_BEAN_NAME)); // 封装 <处理类> AspectJWeavingEnabler 并注册 注册到容器中??
// BeanDefinitionHolder
} if (isBeanConfigurerAspectEnabled(parserContext.getReaderContext().getBeanClassLoader())) {// 检查是否已经注册成功
new SpringConfiguredBeanDefinitionParser().parse(element, parserContext);
}
}
}

经过上述步骤,解析器 已经被封装为BeanDefinition并注册到了容器中,那么要怎样使用它呢,

查看AbstractApplicationContext 类,如下为织入前的准备工作,将相关的后处理器链注入

/**
* Configure the factory's standard context characteristics,
* such as the context's ClassLoader and post-processors.
* @param beanFactory the BeanFactory to configure
*/
protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
/*
* 其它无关代码不再列出
*/ // 增加 对 AspectJ 的支持 AOP 相关后处理器 load-time-weaving 静态织入
// 容器中查找 beanName为 loadTimeWeaver 的 类,如果存在即可说明 静态织入开关存在
if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {// loadTimeWeaver
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));// 注册 aop 相关后处理器
// Set a temporary ClassLoader for type matching.
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}
/*
* 其它无关代码不再列出
*/
}

2.织入

LoadTimeWeaverAwareProcessor 类层次结构图

既然有后置处理器接口,那就在原类中找如下两个方法,自然就能找到静态织入的执行逻辑了。

初始化前会调用 后处理器 LoadTimeWeaverAwareProcessor 的 postProcessBeforeInitialization() 方法

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// 该后处理器 仅仅对 AspectJWeavingEnabler 类型的 bean 起作用
if (bean instanceof LoadTimeWeaverAware) { // 它的实现类只有 AspectJWeavingEnabler 处理器类
// LoadTimeWeaver 实现类为:DefaultContextLoadTimeWeaver
// 至于为什么是它,可以全局搜索,静态织入开关的标签解析器:
// oad-time-weaver -> LoadTimeWeaverBeanDefinitionParser -> DEFAULT_LOAD_TIME_WEAVER_CLASS_NAME
LoadTimeWeaver ltw = this.loadTimeWeaver;
if (ltw == null) {
// 最终获得一个 DefaultContextLoadTimeWeaver 类型 bean
ltw = this.beanFactory.getBean(ConfigurableApplicationContext.LOAD_TIME_WEAVER_BEAN_NAME, LoadTimeWeaver.class);
}
// AspectJWeavingEnabler 持有 loadTimeWeaver
((LoadTimeWeaverAware) bean).setLoadTimeWeaver(ltw);// 注入到 AspectJWeavingEnabler 的 loadTimeWeaver 属性
}
return bean;
}

首先是负责静态织入 (AspectJWeavingEnabler)bean 的类图,是不是又看到了老熟人:BeanFactoryPostProcessor

是不是又看到了老熟人:BeanFactoryPostProcessor 这不是工厂的后置处理器么?

那么再看 AspectJWeavingEnabler 的 postProcessBeanFactory() 方法不就是后续的路子了么?

  • 被注入到 bean 中的 loadTimeWeaver (DefaultContextLoadTimeWeaver) 将是后续的关键

DefaultContextLoadTimeWeaver 类层次结构图

DefaultContextLoadTimeWeaver 实现了接口 BeanClassLoaderAware ,那么它的 setBeanClassLoader() 方法将会在初始化的时候被调用,

从方法中可以看到,DefaultContextLoadTimeWeaver 类型的 bean 的loadTimeWeaver 属性设置为:InstrumentationLoadTimeWeaver 类型的bean,

而最终实现静态AOP织入(修改方法字节码) 的就是它(InstrumentationLoadTimeWeaver)

InstrumentationLoadTimeWeaver

@Override
public void setBeanClassLoader(ClassLoader classLoader) {
LoadTimeWeaver serverSpecificLoadTimeWeaver = createServerSpecificLoadTimeWeaver(classLoader);
if (serverSpecificLoadTimeWeaver != null) {
this.loadTimeWeaver = serverSpecificLoadTimeWeaver;
}
else if (InstrumentationLoadTimeWeaver.isInstrumentationAvailable()) {// 检查当前虚拟机中的 Instrumentation 实例是否可用
this.loadTimeWeaver = new InstrumentationLoadTimeWeaver(classLoader);
}
else {
this.loadTimeWeaver = new ReflectiveLoadTimeWeaver(classLoader);
if (logger.isInfoEnabled()) {
logger.info("Using a reflective load-time weaver for class loader: " +
this.loadTimeWeaver.getInstrumentableClassLoader().getClass().getName());
}
}
}

最后,AspectJWeavingEnabler 实现了 BeanFactoryPostProcessor 接口,那么在所有bean解析结束后会调用其:

postProcessBeanFactory()方法 -> enableAspectJWeaving()

AspectJWeavingEnabler 类层次结构图

public static void enableAspectJWeaving(
@Nullable LoadTimeWeaver weaverToUse, @Nullable ClassLoader beanClassLoader) { if (weaverToUse == null) {
if (InstrumentationLoadTimeWeaver.isInstrumentationAvailable()) {// 当前虚拟机的 Instrumentation 是否可用
weaverToUse = new InstrumentationLoadTimeWeaver(beanClassLoader);
}
else {
throw new IllegalStateException("No LoadTimeWeaver available");
}
}
weaverToUse.addTransformer(
new AspectJClassBypassingClassFileTransformer(new ClassPreProcessorAgentAdapter()));
}

最终,看到了实现的代码:

/**
* ClassFileTransformer decorator that suppresses processing of AspectJ
* classes in order to avoid potential LinkageErrors.
* @see org.springframework.context.annotation.LoadTimeWeavingConfiguration
*/
private static class AspectJClassBypassingClassFileTransformer implements ClassFileTransformer { private final ClassFileTransformer delegate; public AspectJClassBypassingClassFileTransformer(ClassFileTransformer delegate) {
this.delegate = delegate;
} @Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if (className.startsWith("org.aspectj") || className.startsWith("org/aspectj")) {
return classfileBuffer;
}
// 具体逻辑委托给代理
return this.delegate.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer);
}
}
  • LoadTimeWeaverAwareProcessor.postProcessBeforeInitialization() 方法将 DefaultContextLoadTimeWeaver 注册到 AspectJWeavingEnabler 的 loadTimeWeaver 属性中
  • DefaultContextLoadTimeWeaver 实现了接口 BeanClassLoaderAware(实现了该接口的 bean 在 AbstractAutowireCapableBeanFactory.invokeAwareMethods 调用时,

    会去调用:BeanClassLoaderAware.setBeanClassLoader ,该方法逻辑内会把代表当前虚拟机的 InstrumentationLoadTimeWeaver实例,注册到 DefaultContextLoadTimeWeaver 的 loadTimeWeaver属性)

Instrumentation 关联的是当前虚拟机实例,由它去处理静态织入(修改方法字节码),前边通过AOP 定义的,满足条件的切面,将以字节码的形式注入到目标bean中

最终会存在如下的引用关系

AspectJWeavingEnabler.loadTimeWeaver = DefaultContextLoadTimeWeaver.Instance();

DefaultContextLoadTimeWeaver.loadTimeWeaver = InstrumentationLoadTimeWeaver.Instance();

结语

  • OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

  • 使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

引用文章

[1] https://www.cnblogs.com/hongwz/p/5764917.html

Spring源码之-AOP的更多相关文章

  1. Spring 源码学习——Aop

    Spring 源码学习--Aop 什么是 AOP 以下是百度百科的解释:AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程通过预编译的方式和运行期动态代理实 ...

  2. Spring源码之AOP的使用

    Spring往期精彩文章 Spring源码搭建 Spring源码阅读一 前言 我们都知道Java是一门面向对象(OOP)的语言,所谓万物皆对象.但是它也存在着一些个弊端:当你需要给多个不具有继承关系的 ...

  3. 专治不会看源码的毛病--spring源码解析AOP篇

    昨天有个大牛说我啰嗦,眼光比较细碎,看不到重点.太他爷爷的有道理了!要说看人品,还是女孩子强一些.原来记得看到一个男孩子的抱怨,说怎么两人刚刚开始在一起,女孩子在心里就已经和他过完了一辈子.哥哥们,不 ...

  4. Spring源码解析-AOP简单分析

    AOP称为面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等等,不需要去修改业务相关的代码. 对于这部分内容,同样采用一个简单的例子和源码来说明. 接口 public ...

  5. 面试真题--------spring源码解析AOP

    接着上一章对IOC的理解之后,再看看AOP的底层是如何工作的. 1.实现AOP的过程    首先我们要明白,Spring中实现AOP,就是生成一个代理,然后在使用的时候调用代理. 1.1 创建代理工厂 ...

  6. spring源码解读-aop

    aop是指面向切面编程,ProxyFactoryBean是spring aop的底层实现与源头,为什么这么说呢?首先我们看一段配置: 1.target是目标对象,需要对其进行切面增强 2.proxyI ...

  7. Spring源码学习

    Spring源码学习--ClassPathXmlApplicationContext(一) spring源码学习--FileSystemXmlApplicationContext(二) spring源 ...

  8. spring源码分析(二)Aop

    创建日期:2016.08.19 修改日期:2016.08.20-2016.08.21 交流QQ:992591601 参考资料:<spring源码深度解析>.<spring技术内幕&g ...

  9. spring源码学习之路---深入AOP(终)

    作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为保证新博客中博文的完整性,特复制到此留存,如需转载请注明新博客地址即可. 上一章和各位一起看了一下sp ...

  10. spring源码学习之路---AOP初探(六)

    作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为保证新博客中博文的完整性,特复制到此留存,如需转载请注明新博客地址即可. 最近工作很忙,但当初打算学习 ...

随机推荐

  1. [转帖]两种Nginx日志切分方案,狼厂主要在用第1种

    两种Nginx日志切分方案,狼厂主要在用第1种 nginx的日志切分问题一直是运维nginx时需要重点关注的.本文将简单说明下nginx支持的两种日志切分方式. 一.定时任务切分 所谓的定时任务切分, ...

  2. Docker导出镜像的总结

    Docker导出镜像的总结 安装Docker mkdir -p /etc/docker cat >/etc/docker/daemon.josn <<EOF { "bip& ...

  3. [转帖]win10下使用Rclone将OneDrive映射到本地磁盘教程(开机自动挂载)

    win10下使用Rclone将OneDrive映射到本地磁盘教程(开机自动挂载) 下载rclone,winfsp和Git bash Rclone. Winfsp. 和 [Git bash](https ...

  4. [转帖]XCopy命令实现增量备份

    https://www.cnblogs.com/pachongshangdexuebi/p/5051977.html xcopy XCOPY是COPY的扩展,可以把指定的目录连文件和目录结构一并拷贝, ...

  5. [转帖]CertUtil: -hashfile 失败: 0xd00000bb (-805306181)

    https://www.cnblogs.com/heenhui2016/p/de.html 使用CertUtil验证Python安装文件的时候出现了这个错误. CertUtil: -hashfile ...

  6. 查找linux下面某目录下重名出现的文件以及次数

    find . -name '*.data' -exec basename {} \;| sort | uniq -w32 --all-repeated=separate | uniq -c | sor ...

  7. webpack配置scss

    安装依赖: cnpm i sass-loader -D cnpm i node-sass -D node-sass尽量去使用cnpm去安装 创建index2.scss文件 div { h2 { bac ...

  8. vm-storage在新metric占整体1%情况下的写入性能测试

    作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 根据正式环境实际的数据统计,全新的metric占整体的me ...

  9. 【K哥爬虫普法】一个人、一年半、挣了2000万!

    我国目前并未出台专门针对网络爬虫技术的法律规范,但在司法实践中,相关判决已屡见不鲜,K 哥特设了"K哥爬虫普法"专栏,本栏目通过对真实案例的分析,旨在提高广大爬虫工程师的法律意识, ...

  10. kettle系统列文章03---如何建立一个作业

    上篇文章我们建立好了转换,我们希望这个转换可以做成定时任务,每一分钟执行一次 第一步:创建作业开始节点:文件---->新建---->作业----核心对象---->通用-----> ...