Spring源码之-AOP
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的方式
- 动态代理
JDK 动态代理
CGLIB 代理
动态代理,在运行时动态织入增强,所以会对运行性能造成损耗。
- 静态代理
在虚拟机加载字节码文件的时候,将增强内容织入到方法的字节码中,它提供了更细粒度的控制,对性能影响较小。
二、动态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的更多相关文章
- Spring 源码学习——Aop
Spring 源码学习--Aop 什么是 AOP 以下是百度百科的解释:AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程通过预编译的方式和运行期动态代理实 ...
- Spring源码之AOP的使用
Spring往期精彩文章 Spring源码搭建 Spring源码阅读一 前言 我们都知道Java是一门面向对象(OOP)的语言,所谓万物皆对象.但是它也存在着一些个弊端:当你需要给多个不具有继承关系的 ...
- 专治不会看源码的毛病--spring源码解析AOP篇
昨天有个大牛说我啰嗦,眼光比较细碎,看不到重点.太他爷爷的有道理了!要说看人品,还是女孩子强一些.原来记得看到一个男孩子的抱怨,说怎么两人刚刚开始在一起,女孩子在心里就已经和他过完了一辈子.哥哥们,不 ...
- Spring源码解析-AOP简单分析
AOP称为面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等等,不需要去修改业务相关的代码. 对于这部分内容,同样采用一个简单的例子和源码来说明. 接口 public ...
- 面试真题--------spring源码解析AOP
接着上一章对IOC的理解之后,再看看AOP的底层是如何工作的. 1.实现AOP的过程 首先我们要明白,Spring中实现AOP,就是生成一个代理,然后在使用的时候调用代理. 1.1 创建代理工厂 ...
- spring源码解读-aop
aop是指面向切面编程,ProxyFactoryBean是spring aop的底层实现与源头,为什么这么说呢?首先我们看一段配置: 1.target是目标对象,需要对其进行切面增强 2.proxyI ...
- Spring源码学习
Spring源码学习--ClassPathXmlApplicationContext(一) spring源码学习--FileSystemXmlApplicationContext(二) spring源 ...
- spring源码分析(二)Aop
创建日期:2016.08.19 修改日期:2016.08.20-2016.08.21 交流QQ:992591601 参考资料:<spring源码深度解析>.<spring技术内幕&g ...
- spring源码学习之路---深入AOP(终)
作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为保证新博客中博文的完整性,特复制到此留存,如需转载请注明新博客地址即可. 上一章和各位一起看了一下sp ...
- spring源码学习之路---AOP初探(六)
作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为保证新博客中博文的完整性,特复制到此留存,如需转载请注明新博客地址即可. 最近工作很忙,但当初打算学习 ...
随机推荐
- [转帖]两种Nginx日志切分方案,狼厂主要在用第1种
两种Nginx日志切分方案,狼厂主要在用第1种 nginx的日志切分问题一直是运维nginx时需要重点关注的.本文将简单说明下nginx支持的两种日志切分方式. 一.定时任务切分 所谓的定时任务切分, ...
- Docker导出镜像的总结
Docker导出镜像的总结 安装Docker mkdir -p /etc/docker cat >/etc/docker/daemon.josn <<EOF { "bip& ...
- [转帖]win10下使用Rclone将OneDrive映射到本地磁盘教程(开机自动挂载)
win10下使用Rclone将OneDrive映射到本地磁盘教程(开机自动挂载) 下载rclone,winfsp和Git bash Rclone. Winfsp. 和 [Git bash](https ...
- [转帖]XCopy命令实现增量备份
https://www.cnblogs.com/pachongshangdexuebi/p/5051977.html xcopy XCOPY是COPY的扩展,可以把指定的目录连文件和目录结构一并拷贝, ...
- [转帖]CertUtil: -hashfile 失败: 0xd00000bb (-805306181)
https://www.cnblogs.com/heenhui2016/p/de.html 使用CertUtil验证Python安装文件的时候出现了这个错误. CertUtil: -hashfile ...
- 查找linux下面某目录下重名出现的文件以及次数
find . -name '*.data' -exec basename {} \;| sort | uniq -w32 --all-repeated=separate | uniq -c | sor ...
- webpack配置scss
安装依赖: cnpm i sass-loader -D cnpm i node-sass -D node-sass尽量去使用cnpm去安装 创建index2.scss文件 div { h2 { bac ...
- vm-storage在新metric占整体1%情况下的写入性能测试
作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 根据正式环境实际的数据统计,全新的metric占整体的me ...
- 【K哥爬虫普法】一个人、一年半、挣了2000万!
我国目前并未出台专门针对网络爬虫技术的法律规范,但在司法实践中,相关判决已屡见不鲜,K 哥特设了"K哥爬虫普法"专栏,本栏目通过对真实案例的分析,旨在提高广大爬虫工程师的法律意识, ...
- kettle系统列文章03---如何建立一个作业
上篇文章我们建立好了转换,我们希望这个转换可以做成定时任务,每一分钟执行一次 第一步:创建作业开始节点:文件---->新建---->作业----核心对象---->通用-----> ...