spring(五):AOP
AOP(Aspect Oriented Programming)
面向切面编程,是一种编程范式,提供从另一个角度来考虑程序结构从而完善面向对象编程(OOP)。
在进行OOP开发时,都是基于对组件(比如类)进行开发,然后对组件进行组合,OOP最大问题就是无法解耦组件进行开发。
AOP为开发者提供一种进行横切关注点(比如日志关注点横切了支付关注点)分离并织入的机制,把横切关注点分离,然后通过某种技术织入到系统中,从而无耦合的完成了我们的功能。
横切关注点可能包含很多,比如非业务的:日志、事务处理、缓存、性能统计、权限控制等;还可能是业务的:如某个业务组件横切于多个模块。
基本概念
- 连接点(Jointpoint):表示需要在程序中插入横切关注点的扩展点,连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等,Spring只支持方法执行连接点;“在哪里干”;
- 切入点(Pointcut):选择一组相关连接点的模式,即可以认为连接点的集合,Spring默认使用AspectJ语法;“在哪里干的集合”;[ <aop:aspectj-autoproxy/> 开启spring对@Aspectj的支持]
- 通知(Advice):在连接点上执行的行为;“干什么”;
- 引入/内部类型声明(inter-type declaration):为已有的类添加额外新的字段或方法,Spring允许引入新的接口(必须对应一个实现)到所有被代理对象(目标对象);“干什么(引入什么)”;
- 切面(Aspect):横切关注点的模块化,比如日志组件。可以认为是通知、引入和切入点的组合,在Spring中可以使用Schema和@AspectJ方式进行组织实现;“在哪干和干什么集合”;
- 目标对象(Target Object):需要被织入横切关注点的对象,被代理对象;“对谁干”;
- 织入(Weaving):织入是一个过程,是将切面应用到目标对象从而创建出AOP代理对象的过程,织入可以在编译期、类装载期、运行期进行。
- 编译时织入,生成完整功能的Java字节码,需特殊编译器(AspectJ)
- 类加载时织入,AspectJ、AspectWerkz
- 运行时织入,动态代理。@EnableAspectJAutoProxy。[Spring采用运行时]
- AOP代理(AOP Proxy):AOP框架使用代理模式创建的对象,从而实现在连接点处插入通知,就是通过代理来对目标对象应用切面。
What-@Aspect
Where-@Pointcut
When-@Advice(@Before、@After、@AfterReturning、@AfterThrowing、@Around)
在Spring中通过代理模式实现AOP,并通过拦截器模式以环绕连接点的拦截器链织入通知(即应用切面)
通知类型
- 前置通知(Before Advice):在切入点选择的连接点处的方法之前执行的通知,该通知不影响正常程序执行流程(除非该通知抛出异常,该异常将中断当前方法链的执行而返回)。
- 后置通知(After Advice):在切入点选择的连接点处的方法之后执行的通知,包括如下类型的后置通知:
- 后置返回通知(After returning Advice):在切入点选择的连接点处的方法正常执行完毕时执行的通知,必须是连接点处的方法没抛出任何异常正常返回时才调用后置通知。
- 后置异常通知(After throwing Advice): 在切入点选择的连接点处的方法抛出异常返回时执行的通知,必须是连接点处的方法抛出任何异常返回时才调用异常通知。
- 后置最终通知(After finally Advice): 在切入点选择的连接点处的方法返回时执行的通知,不管抛没抛出异常都执行,类似于Java中的finally块。
- 环绕通知(Around Advices):环绕着在切入点选择的连接点处的方法所执行的通知,环绕通知可以在方法调用之前和之后自定义任何行为,并且可以决定是否执行连接点处的方法、替换返回值、抛出异常等等。
通知顺序
Spring中可以通过在切面实现类上实现org.springframework.core.Ordered接口或使用Order注解来指定切面优先级。在多个切面中,Ordered.getValue()方法返回值(或者注解值)较小值的那个切面拥有较高优先级。
@Order(2)
动态代理
如果目标对象实现了接口,默认使用JDK,可以强制用CGLIB;否则,采用CGLIB。
JDK动态代理
只能为接口创建动态代理实例,而不能针对类。
使用java.lang.reflect.Proxy动态代理实现,通过调用目标类的getClass().getInterfaces()方法获取目标对象的接口信息,并生成一个实现了代理接口的动态代理class,然后通过反射技术获得该class的构造函数,并利用构造函数生成实例,在调用具体方法前调用InvocationHandler处理。
在Proxy这个类当中首先实例化一个对象ProxyClassFactory,然后在get方法中调用了apply方法,完成对代理类的创建:
- generateProxyClass通过反射收集字段和属性然后生成字节
- defineClass0 jvm内部完成对上述字节的load
CGLIB动态代理
主要是对指定的类生成一个子类,覆盖其中的方法。(不能通知final方法)
利用asm开源包,采用字节码技术,为一个类创建子类,在子类中采用方法拦截(MethodInterceptor @override intercept()),拦截所有父类方法的调用,顺势织入横切逻辑。
会产生两次构造器调用,第一次是目标类的构造器调用,第二次是CGLIB生成的代理类的构造器调用。
设计分析
1 为目标对象建立AopProxy代理对象
在依赖注入时,实例化bean后都会调用getObjectForBeanInstance方法,这里就是处理FactoryBean的入口。
通过配置和调用ProxyFactoryBean来完成代理对象的创建。
配置通知器advisor、proxyFactoryBean(目标对象target、interceptorNames拦截器数组、目标对象接口数组)
// ProxyFactoryBean
@Nullable
public Object getObject() throws BeansException {
// 对通知器链进行初始化
// 通知器链封装了一系列的拦截器(需要从配置中读取)
// 然后为代理对象的生成做准备
this.initializeAdvisorChain();
// 区分两种类型的Bean
if (this.isSingleton()) {
return this.getSingletonInstance();
} else {
...
return this.newPrototypeInstance();
}
}
private synchronized void initializeAdvisorChain() throws AopConfigException, BeansException {
// 通知器链未初始化(初始化工作发生在应用第一次通过ProxyFactoryBean获取代理对象的时候)
if (!this.advisorChainInitialized) {
if (!ObjectUtils.isEmpty(this.interceptorNames)) {
...
String[] var1 = this.interceptorNames;
int var2 = var1.length;
for(int var3 = 0; var3 < var2; ++var3) {
String name = var1[var3];
...
if (name.endsWith("*")) {
...
} else {
Object advice;
if (!this.singleton && !this.beanFactory.isSingleton(name)) {
// Prototype类型
advice = new ProxyFactoryBean.PrototypePlaceholderAdvisor(name);
} else {
// singleton类型:通过getBean获取通知器
advice = this.beanFactory.getBean(name);
}
// 将通知器加入拦截器链中
this.addAdvisorOnChainCreation(advice, name);
}
}
}
this.advisorChainInitialized = true;
}
}
private synchronized Object getSingletonInstance() {
if (this.singletonInstance == null) {
this.targetSource = this.freshTargetSource();
if (this.autodetectInterfaces && this.getProxiedInterfaces().length == 0 && !this.isProxyTargetClass()) {
// 判断需要代理的接口
Class<?> targetClass = this.getTargetClass();
...
// 设置代理对象调用接口
this.setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass, this.proxyClassLoader));
}
super.setFrozen(this.freezeProxy);
// createAopProxy:生成aop代理对象 ★调用父类的createAopProxy()
// getProxy:<<AopProxy>>接口的两种实现--JdkDynamicAopProxy、CglibAopProxy
this.singletonInstance = this.getProxy(this.createAopProxy());
}
return this.singletonInstance;
}
private synchronized Object newPrototypeInstance() {
...
ProxyCreatorSupport copy = new ProxyCreatorSupport(this.getAopProxyFactory());
TargetSource targetSource = this.freshTargetSource();
copy.copyConfigurationFrom(this, targetSource, this.freshAdvisorChain());
if (this.autodetectInterfaces && this.getProxiedInterfaces().length == 0 && !this.isProxyTargetClass()) {
// 和上面一模一样的套路啊。。。
Class<?> targetClass = targetSource.getTargetClass();
if (targetClass != null) {
copy.setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass, this.proxyClassLoader));
}
}
...
// 然后通过<<AopProxy>>接口的两种实现--JdkDynamicAopProxy、CglibAopProxy获取代理对象
return this.getProxy(copy.createAopProxy());
}
// ProxyCreatorSupport
protected final synchronized AopProxy createAopProxy() {
if (!this.active) {
this.activate();
}
// createAopProxy:<<AopProxyFactory>>接口的方法
return this.getAopProxyFactory().createAopProxy(this);
}
// 具体实现:DefaultAopProxyFactory
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
// 如果目标对象是接口类,使用jdk
if (!config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) {
return new JdkDynamicAopProxy(config);
} else {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
...
} else {
// ObjenesisCglibAopProxy extends CglibAopProxy
return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config));
}
}
// new的过程都是先从AdvisedSupport对象中取得配置的目标对象进行检查
}
// JdkDynamicAopProxy
public Object getProxy(@Nullable ClassLoader classLoader) {
...
// 首先从advised对象中获取代理对象的代理接口配置
Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
this.findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
// 然后调用Proxy的newProxyInstance方法得到Proxy代理对象
// 需要三个参数:类加载器、代理接口、回调方法所在对象
// 回调方法用的是InvokeHandler的invoke回调入口
return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}
// CglibAopProxy
public Object getProxy(@Nullable ClassLoader classLoader) {
...
try {
// 从advised对象中获取配置的target对象
Class<?> rootClass = this.advised.getTargetClass();
...
Class<?> proxySuperClass = rootClass;
int x;
if (ClassUtils.isCglibProxyClass(rootClass)) {
proxySuperClass = rootClass.getSuperclass();
Class<?>[] additionalInterfaces = rootClass.getInterfaces();
Class[] var5 = additionalInterfaces;
int var6 = additionalInterfaces.length;
for(x = 0; x < var6; ++x) {
Class<?> additionalInterface = var5[x];
this.advised.addInterface(additionalInterface);
}
}
// 验证代理对象的接口设置
this.validateClassIfNecessary(proxySuperClass, classLoader);
// 创建并配置cglib的enhancer
Enhancer enhancer = this.createEnhancer();
if (classLoader != null) {
enhancer.setClassLoader(classLoader);
if (classLoader instanceof SmartClassLoader && ((SmartClassLoader)classLoader).isClassReloadable(proxySuperClass)) {
enhancer.setUseCache(false);
}
}
// 设置enhancer的代理接口、回调方法等
enhancer.setSuperclass(proxySuperClass);
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setStrategy(new CglibAopProxy.ClassLoaderAwareUndeclaredThrowableStrategy(classLoader));
Callback[] callbacks = this.getCallbacks(rootClass);
Class<?>[] types = new Class[callbacks.length];
for(x = 0; x < types.length; ++x) {
types[x] = callbacks[x].getClass();
}
enhancer.setCallbackFilter(new CglibAopProxy.ProxyCallbackFilter(this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
enhancer.setCallbackTypes(types);
// 通过enhancer生成代理对象 ★
// 注意这里有回调
return this.createProxyClassAndInstance(enhancer, callbacks);
}...
}
// 这里是回调方法,通过设置DynamicAdvisedInterceptor拦截器完成AOP功能
private Callback[] getCallbacks(Class<?> rootClass) throws Exception {
...
Callback aopInterceptor = new CglibAopProxy.DynamicAdvisedInterceptor(this.advised);
...
return callbacks;
}
2 启动代理对象的拦截器来完成各种横切面的织入
在第一步的过程中,拦截器已经配置到代理对象中,它是通过回调方法起作用。
JdkDynamicAopProxy的invoke拦截
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;
TargetSource targetSource = this.advised.targetSource;
Object target = null;
Boolean var8;
try {
if (this.equalsDefined || !AopUtils.isEqualsMethod(method)) {
...
// 得到目标对象
target = targetSource.getTarget();
Class<?> targetClass = target != null ? target.getClass() : null;
// 这个方法对象注册了拦截器
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
// 没有设定拦截器链,直接调用target对象的方法
if (chain.isEmpty()) {
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
// 对target对象的方法调用是通过反射机制,然后使用invoke调用方法反射对象
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
} else {
// 拦截,通过ReflectiveMethodInvocation
MethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// 沿着拦截器链继续前进
retVal = invocation.proceed();
}
...
}
var8 = this.equals(args[0]);
}...
return var8;
}
CglibAopProxy的DynamicAdvisedInterceptor的intercept拦截
@Nullable
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
...
try {
...
// 得到目标对象
target = targetSource.getTarget();
Class<?> targetClass = target != null ? target.getClass() : null;
// 从advised取得配置好的AOP通知
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
Object retVal;
// 若没有,则直接调用target对象的方法
if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = methodProxy.invoke(target, argsToUse);
} else {
// 拦截,通过CglibMethodInvocation启动advice通知
// CglibMethodInvocation extends ReflectiveMethodInvocation
// 沿着拦截器链继续前进
retVal = (new CglibAopProxy.CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy)).proceed();
}
retVal = CglibAopProxy.processReturnType(proxy, target, method, retVal);
var16 = retVal;
}...
return var16;
}
综上,都是从advised取得拦截器链的。
拦截都是通过ReflectiveMethodInvocation的proceed方法实现的。
如何将拦截器链配置进advised的?
拦截器是ReflectiveMethodInvocation类中一个名为interceptorsAndDynamicMethodMatchers的List中的元素。
this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
advised的getInterceptorsAndDynamicInterceptionAdvice是由advisorChainFactory实现的,它的具体类型是DefaultAdvisorChainFactory。
DefaultAdvisorChainFactory通过AdvisorAdapterRegistry来适配ProxyFactoryBean中得到的通知器,注册拦截器。
在ProxyFactoryBean的getObject方法中对advisor进行初始化时,从配置中获取了通知器。(通过实现BeanFactoryAware接口,设置回调方法委托给IoC容器)
proceed方法
@Nullable
public Object proceed() throws Throwable {
// 如果已经到拦截器链的末尾,直接调用目标对象的方法
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return this.invokeJoinpoint();
} else {
// 否则,得到下一个拦截器
Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
// 通过拦截器进行matches判断是否适用于横切增强的场合
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher)interceptorOrInterceptionAdvice;
Class<?> targetClass = this.targetClass != null ? this.targetClass : this.method.getDeclaringClass();
// 如果是,从拦截器得到通知器,并启动invoke方法
// 否则,迭代调用proceed方法
return dm.methodMatcher.matches(this.method, targetClass, this.arguments) ? dm.interceptor.invoke(this) : this.proceed();
} else {
return ((MethodInterceptor)interceptorOrInterceptionAdvice).invoke(this);
}
}
}
advice通知是如何实现的?
DefaultAdvisorChainFactory通过AdvisorAdapterRegistry来适配ProxyFactoryBean中得到的通知器,注册拦截器。
// DefaultAdvisorChainFactory
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Advised config, Method method, @Nullable Class<?> targetClass) {
// GlobalAdvisorAdapterRegistry是一个单例,作用是适配器
AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance();
Advisor[] advisors = config.getAdvisors();
...
for(int var11 = 0; var11 < var10; ++var11) {
Advisor advisor = var9[var11];
if (advisor instanceof PointcutAdvisor) {
PointcutAdvisor pointcutAdvisor = (PointcutAdvisor)advisor;
if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
...
if (match) {
MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
...
}
}
} else if (advisor instanceof IntroductionAdvisor) {
IntroductionAdvisor ia = (IntroductionAdvisor)advisor;
if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) {
Interceptor[] interceptors = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors));
}
} else {
Interceptor[] interceptors = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors));
}
}
return interceptorList;
}
MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
封装着advice织入实现的入口。
// DefaultAdvisorAdapterRegistry
public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {
List<MethodInterceptor> interceptors = new ArrayList(3);
Advice advice = advisor.getAdvice();
if (advice instanceof MethodInterceptor) {
interceptors.add((MethodInterceptor)advice);
}
// 对通知进行适配
Iterator var4 = this.adapters.iterator();
while(var4.hasNext()) {
AdvisorAdapter adapter = (AdvisorAdapter)var4.next();
// 如果适配器支持某种通知,则从对应的适配器中获取该类通知器的拦截器
if (adapter.supportsAdvice(advice)) {
interceptors.add(adapter.getInterceptor(advisor));
}
}
...
}
我们知道,proceed处理拦截器的时候是通过dm.interceptor.invoke(this)
拦截器的回调方法。下面举一个例子。
class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable {
MethodBeforeAdviceAdapter() {
}
public boolean supportsAdvice(Advice advice) {
return advice instanceof MethodBeforeAdvice;
}
public MethodInterceptor getInterceptor(Advisor advisor) {
MethodBeforeAdvice advice = (MethodBeforeAdvice)advisor.getAdvice();
return new MethodBeforeAdviceInterceptor(advice);
}
}
public class MethodBeforeAdviceInterceptor implements MethodInterceptor, BeforeAdvice, Serializable {
private final MethodBeforeAdvice advice;
public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) {
Assert.notNull(advice, "Advice must not be null");
this.advice = advice;
}
// 先触发advice的before,然后才是proceed调用!
// 因为这是method before!如果是其他的拦截器,顺序、具体实现又不一样了
public Object invoke(MethodInvocation mi) throws Throwable {
this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
return mi.proceed();
}
}
spring(五):AOP的更多相关文章
- Spring(五)AOP简述
一.AOP简述 AOP全称是:aspect-oriented programming,它是面向切面编号的思想核心, AOP和OOP既面向对象的编程语言,不相冲突,它们是两个相辅相成的设计模式型 AOP ...
- 跟着刚哥学习Spring框架--AOP(五)
AOP AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善.OOP引入 ...
- Spring框架第五篇之Spring与AOP
一.AOP概述 AOP(Aspect Orient Programming),面向切面编程,是面向对象编程OOP的一种补充.面向对象编程是从静态角度考虑程序的结构,而面向切面编程是从动态角度考虑程序运 ...
- Spring 学习十五 AOP
http://www.hongyanliren.com/2014m12/22797.html 1: 通知(advice): 就是你想要的功能,也就是安全.事物.日子等.先定义好,在想用的地方用一下.包 ...
- Spring学习之旅(五)--AOP
什么是 AOP AOP(Aspect-OrientedProgramming,面向方面编程),可以说是 OOP(Object-Oriented Programing,面向对象编程)的补充和完善. OO ...
- Spring实现AOP的4种方式
了解AOP的相关术语:1.通知(Advice):通知定义了切面是什么以及何时使用.描述了切面要完成的工作和何时需要执行这个工作.2.连接点(Joinpoint):程序能够应用通知的一个“时机”,这些“ ...
- spring的AOP
最近公司项目中需要添加一个日志记录功能,就是可以清楚的看到谁在什么时间做了什么事情,因为项目已经运行很长时间,这个最初没有开来进来,所以就用spring的面向切面编程来实现这个功能.在做的时候对spr ...
- Spring 实践 -AOP
Spring 实践 标签: Java与设计模式 AOP引介 AOP(Aspect Oriented Programing)面向切面编程采用横向抽取机制,以取代传统的纵向继承体系的重复性代码(如性能监控 ...
- Spring实现AOP的4种方式(转)
转自:http://blog.csdn.net/udbnny/article/details/5870076 Spring实现AOP的4种方式 先了解AOP的相关术语:1.通知(Advice):通知定 ...
- spring(二) AOP之AspectJ框架的使用
前面讲解了spring的特性之一,IOC(控制反转),因为有了IOC,所以我们都不需要自己new对象了,想要什么,spring就给什么.而今天要学习spring的第二个重点,AOP.一篇讲解不完,所以 ...
随机推荐
- WebApp开发-Zepto
zepto.js自己去官网下载哈. DOM操作 $(document).ready(function(){ var $cr = $("<div class='cr'>插入的div ...
- vim和emacs
vim和emacs 在编程界一直有两大神器的传说.这两大神器一个是emacs,一个是vim.一个是神的编辑器,一个是编辑器之神. 程序员的圈子里面也一直流传着一个段子,说是世界上的程序员分为三种.使用 ...
- Linux系统目录结构和常用目录主要存放内容的说明
目录结构图 常用目录 /: 根目录 一般根目录下只存放目录,在 linux 下有且只有一个根目录,所有的东西都是从这里开始 当在终端里输入 /home,其实是在告诉电脑,先从 /(根目录)开始,再进入 ...
- 【35】单层卷积网络(simple convolution)
今天我们要讲的是如何构建卷积神经网络的卷积层,下面来看个例子. 上节课,我们已经讲了如何通过两个过滤器卷积处理一个三维图像,并输出两个不同的4×4矩阵.假设使用第一个过滤器进行卷积,得到第一个4× ...
- navicat连接mysql出现2059错误的解决方法
安装navicat之后新建连接出现了2059的错误 网上查询过后,发现这个错误出现的原因是在mysql8之前的版本中加密规则为mysql_native_password,而在mysql8以后的加密规则 ...
- Qt多线程实现思路二
建立一个继承于Qobject的类myThread 在类myThread中定义线程处理函数不必是思路一里的run(); 在窗口类中开辟一个自定义线程myThread的指针对象myT = new myTh ...
- Django内置的中间件
内置中间件 1. django.middleware.gzip.GZipMiddleware:相应数据进行压缩.如果内容长度少于200个长度,那么就不会压缩. 在settings.py文件中配置MID ...
- 理解Javascript的变量提升
前言 本文2922字,阅读大约需要8分钟. 总括: 什么是变量提升,使用var,let,const,function,class声明的变量函数类在变量提升的时候都有什么区别. 参考文章:Hoistin ...
- [USACO09JAN]Total Flow【网络流】
Farmer John always wants his cows to have enough water and thus has made a map of the N (1 <= N & ...
- mysql-使用存储过程创建大批量数据
参考:https://www.iteye.com/blog/825635381-2161290 场景1.创建1万个table,每个table种插入1条记录 DELIMITER $$ CREATE DA ...