写在前面

  spring 事务是springAOP 的一个实现。我们以分析spring的事务,来分析spring的AOP实现。

基本知识

  如果目标方法被spring的事务声明,则执行该目标方法的对象就会是spring动态生成的代理对象。如果目标方法的类有接口实现,那么产生的是jdk的动态代理,反之则是CGLIB。(使用了抽象工厂模式

  

代理对象产生过程(以JDK为参考)

  1. 产生代理对象的过程是在spring加载的过程中。(一个代理对象和一个InvocationHandler绑定,当代理对象的方法执行时,就会去执行InvocationHandler的invoke(Object proxy, Method method, Object[] args)方法),在该方法中你可以现在是否处理代理对象的目标方法。

  代理对象的产生主要是ProxyFactory的getProxy()方法,内部通过AOPProxyFactory使用了抽象工厂模式,产生JdkDynamicAopProxy、ObjenesisCglibAopProxy

  抽象工厂的实现类(参见详细过程

  1. public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
  2. public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
  3. if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
  4. Class<?> targetClass = config.getTargetClass();
  5. if (targetClass == null) {
  6. throw new AopConfigException("TargetSource cannot determine target class: " +
  7. "Either an interface or a target is required for proxy creation.");
  8. }
  9. if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
  10. return new JdkDynamicAopProxy(config);
  11. }
  12. return new ObjenesisCglibAopProxy(config);
  13. }
  14. else {
  15. return new JdkDynamicAopProxy(config);
  16. }
  17. }
  18. }

   获取代理对象的过程

  1. final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {
  2. @Override
  3. public Object getProxy(ClassLoader classLoader) {
  4. if (logger.isDebugEnabled()) {
  5. logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());
  6. }
  7. Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised);
  8. findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
  9. return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
  10. }
  11. }

  层级关系如下

  AOPProxyFactory 实现了类 DAOPProxyFactory

  AOPProxy 实现类JdkDynamicAopProxy、CglibAopProxy

执行代理对象的invoke方法

  前面已经讲过,再spring加载的时候,就加载了相关的代理对象。在代理对象执行目标方法之前,会执行与代理对象绑定的InvocationHandler的invoke方法。

  JdkDynamicAopProxy本身就实现了InvocationHandler接口

  1. final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable

  JdkDynamicAopProxy有一个重要的属性

  1. private final AdvisedSupport advised;

  最关键的就是这个AdvisedSupport advised属性,它包含了我们在xml中配置的拦截信息,同时还包含了这个JdkDynamicAopProxy要代理的接口及其实现类,对于本文来说就是XXService和XXServiceImpl。JdkDynamicAopProxy可以根据这些配置信息来创建一个代理对象实现拦截,同时又可以执行XXServiceImpl本身的业务方法。

  AdvisedSupport有三个重要的内容:

    TargetSource是目标类型和目标对象的包裹,在这里是XXServiceImpl类和XXServiceImpl对象。 
    List<Class<?>> interfaces:包含了目标类型实现的接口,在这里就是目标对象XXService 
    List<Advisor> advisors:这里包含了我们在xml文件中配置的所有信息。这一部分是每个AdvisedSupport所共享的信息,而前面两个是每个AdvisedSupport所独有的信息。

回到JdkDynamicAopProxy,来看看拦截过程,即调用代理对象的方法,然后被拦截到代理对象的InvocationHandler的invoke方法,JdkDynamicAopProxy的invoke方法如下:

  1. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  2. MethodInvocation invocation;
  3. Object oldProxy = null;
  4. boolean setProxyContext = false;
  5.  
  6. TargetSource targetSource = this.advised.targetSource;
  7. Class<?> targetClass = null;
  8. Object target = null;
  9.  
  10. try {
  11. if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
  12. // The target does not implement the equals(Object) method itself.
  13. return equals(args[0]);
  14. }
  15. if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
  16. // The target does not implement the hashCode() method itself.
  17. return hashCode();
  18. }
  19. if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
  20. method.getDeclaringClass().isAssignableFrom(Advised.class)) {
  21. // Service invocations on ProxyConfig with the proxy config...
  22. return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
  23. }
  24.  
  25. Object retVal;
  26.        //关注的重点1:
  27. if (this.advised.exposeProxy) {
  28. // Make invocation available if necessary.
  29. oldProxy = AopContext.setCurrentProxy(proxy);
  30. setProxyContext = true;
  31. }
  32.  
  33. // May be null. Get as late as possible to minimize the time we "own" the target,
  34. // in case it comes from a pool.
  35. target = targetSource.getTarget();
  36. if (target != null) {
  37. targetClass = target.getClass();
  38. }
  39.  
  40. // Get the interception chain for this method.
           //关注的重点2 
  41. List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
  42.  
  43. // Check whether we have any advice. If we don't, we can fallback on direct
  44. // reflective invocation of the target, and avoid creating a MethodInvocation.
  45. if (chain.isEmpty()) {
  46. // We can skip creating a MethodInvocation: just invoke the target directly
  47. // Note that the final invoker must be an InvokerInterceptor so we know it does
  48. // 关注重点3:
  49. retVal = AopUtils.invokeJoinpointUsingReflection(target, method, args);
  50. }
  51. else {
  52. // 关注的重点4
  53. invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
  54. // 关注的重点5
  55. retVal = invocation.proceed();
  56. }
  57.  
  58. // Massage return value if necessary.
  59. Class<?> returnType = method.getReturnType();
  60. if (retVal != null && retVal == target && returnType.isInstance(proxy) &&
  61. !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
  62. retVal = proxy;
  63. }
  64. else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
  65. throw new AopInvocationException(
  66. "Null return value from advice does not match primitive return type for: " + method);
  67. }
  68. return retVal;
  69. }
  70. finally {
  71. if (target != null && !targetSource.isStatic()) {
  72. // Must have come from TargetSource.
  73. targetSource.releaseTarget(target);
  74. }
  75. if (setProxyContext) {
  76. // Restore old proxy.
  77. AopContext.setCurrentProxy(oldProxy);
  78. }
  79. }
  80. }

  关注的重点1:this.advised.exposeProxy即我们在xml文件中所配置的<aop:config expose-proxy="false">。如果配置为true,默认false,则意味着在该线程内将会暴露proxy代理对象,实现共享,即在该线程中的任何地方都可以都可以取到proxy代理对象。具体是由ThreadLocal设计模式来实现的

  关注的重点2:根据我们的目标类和方法找到对应的拦截器链 List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); 它内部是通过advised的一个this.advisorChainFactory来实现这一过程,advisorChainFactory默认为DefaultAdvisorChainFactory,实现过程如下:

  1. public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, Class<?> targetClass) {
  2. MethodCacheKey cacheKey = new MethodCacheKey(method);
  3. List<Object> cached = this.methodCache.get(cacheKey);
  4. if (cached == null) {
  5. cached = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(
  6. this, method, targetClass);
  7. this.methodCache.put(cacheKey, cached);
  8. }
  9. return cached;
  10. }
  1. public List<Object> getInterceptorsAndDynamicInterceptionAdvice(
  2. Advised config, Method method, Class<?> targetClass) {
  3.  
  4. // This is somewhat tricky... We have to process introductions first,
  5. // but we need to preserve order in the ultimate list.
  6. List<Object> interceptorList = new ArrayList<Object>(config.getAdvisors().length);
  7. Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass());
  8. boolean hasIntroductions = hasMatchingIntroductions(config, actualClass);
  9. AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance();
  10.  
  11. for (Advisor advisor : config.getAdvisors()) {
  12. if (advisor instanceof PointcutAdvisor) {
  13. // Add it conditionally.
  14. PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;
  15. if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
  16. MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
  17. MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
  18. if (MethodMatchers.matches(mm, method, actualClass, hasIntroductions)) {
  19. if (mm.isRuntime()) {
  20. // Creating a new object instance in the getInterceptors() method
  21. // isn't a problem as we normally cache created chains.
  22. for (MethodInterceptor interceptor : interceptors) {
  23. interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm));
  24. }
  25. }
  26. else {
  27. interceptorList.addAll(Arrays.asList(interceptors));
  28. }
  29. }
  30. }
  31. }
  32. else if (advisor instanceof IntroductionAdvisor) {
  33. IntroductionAdvisor ia = (IntroductionAdvisor) advisor;
  34. if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) {
  35. Interceptor[] interceptors = registry.getInterceptors(advisor);
  36. interceptorList.addAll(Arrays.asList(interceptors));
  37. }
  38. }
  39. else {
  40. Interceptor[] interceptors = registry.getInterceptors(advisor);
  41. interceptorList.addAll(Arrays.asList(interceptors));
  42. }
  43. }
  44.  
  45. return interceptorList;
  46. }

  上述过程分了三种情况来获取对应的Interceptor拦截器,config.getAdvisors()是我们在xml文件中所配置的所有的拦截情况,对于这些所有的拦截情况:

  当Advisor为PointcutAdvisor类型的时: 
    这是我们本工程的配置的拦截,每个拦截都有pointcut,针对这种情况,首先判断该PointcutAdvisor的ClassFilter是否拦截了targetClass,若拦截则需继续判断PointcutAdvisor的MethodMatcher是否拦截targetClass的method方法。如果也拦截了,就需要将PointcutAdvisor的adice添加进去,则继续判断这个PointcutAdvisor的MethodMatcher是否是动态变化的,若是则需要将interceptor进一步包装成InterceptorAndDynamicMethodMatcher然后添加进去。 
  当Advisor为IntroductionAdvisor类型的时候: 
    IntroductionAdvisor应用在类上,不需要判断是否拦截了相应的方法。IntroductionAdvisor只有一个ClassFilter。此时仅仅去判断下是否拦截相应的类即可。 
  其他情况: 
    直接获取相应的interceptor。

  我们来看下根据Advisor来获取对应的MethodInterceptor方法:

  1. public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {
  2. List<MethodInterceptor> interceptors = new ArrayList<MethodInterceptor>(3);
  3. Advice advice = advisor.getAdvice();
  4. if (advice instanceof MethodInterceptor) {
  5. interceptors.add((MethodInterceptor) advice);
  6. }
  7. for (AdvisorAdapter adapter : this.adapters) {
  8. if (adapter.supportsAdvice(advice)) {
  9. interceptors.add(adapter.getInterceptor(advisor));
  10. }
  11. }
  12. if (interceptors.isEmpty()) {
  13. throw new UnknownAdviceTypeException(advisor.getAdvice());
  14. }
  15. return interceptors.toArray(new MethodInterceptor[interceptors.size()]);
  16. }

  首先是判断advisor.getAdvice()是否已实现了MethodInterceptor,如AspectJAfterAdvice、AspectJAfterThrowingAdvice等。 然后又是利用适配器模式,将不用的advice封装成对应的MethodInterceptor。如MethodBeforeAdviceAdapter,默认硬编码注册了三个

  1. public DefaultAdvisorAdapterRegistry() {
  2. registerAdvisorAdapter(new MethodBeforeAdviceAdapter());
  3. registerAdvisorAdapter(new AfterReturningAdviceAdapter());
  4. registerAdvisorAdapter(new ThrowsAdviceAdapter());
  5. }

  看下MethodBeforeAdviceAdapter:

  1. class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable {
  2.  
  3. @Override
  4. public boolean supportsAdvice(Advice advice) {
  5. return (advice instanceof MethodBeforeAdvice);
  6. }
  7.  
  8. @Override
  9. public MethodInterceptor getInterceptor(Advisor advisor) {
  10. MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice();
  11. return new MethodBeforeAdviceInterceptor(advice);
  12. }
  13.  
  14. }

  这就是典型的适配器模式,当Advice为MethodBeforeAdvice时,就会封装成MethodBeforeAdviceInterceptor。 至此获取MethodInterceptor链的过程就完成了,回到List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);即List<Object>是一系列的MethodInterceptor构成的。

关注重点3:在获取MethodInterceptor链后,如果为空,则没有拦截器直接执行目标对象的方法。retVal = AopUtils.invokeJoinpointUsingReflection(target, method, args);中的target对于本工程来说就是XXServiceImpl,所以此方法的本质就是利用反射执行XXServiceImpl的method方法。如下:

  1. public static Object invokeJoinpointUsingReflection(Object target, Method method, Object[] args)
  2. throws Throwable {
  3.  
  4. // Use reflection to invoke the method.
  5. try {
  6. ReflectionUtils.makeAccessible(method);
  7. return method.invoke(target, args);
  8. }
  9. catch (InvocationTargetException ex) {
  10. // Invoked method threw a checked exception.
  11. // We must rethrow it. The client won't see the interceptor.
  12. throw ex.getTargetException();
  13. }
  14. catch (IllegalArgumentException ex) {
  15. throw new AopInvocationException("AOP configuration seems to be invalid: tried calling method [" +
  16. method + "] on target [" + target + "]", ex);
  17. }
  18. catch (IllegalAccessException ex) {
  19. throw new AopInvocationException("Could not access method [" + method + "]", ex);
  20. }
  21. }

关注的重点4: 有了拦截器链后,就构造一个ReflectiveMethodInvocation来完成这一个调用过程

  首先说下接口情况:ReflectiveMethodInvocation实现了ProxyMethodInvocation,ProxyMethodInvocation继承了MethodInvocation, MethodInvocation继承了Invocation, Invocation继承了Joinpoint,此时的Joinpoint是AOP联盟定义的接口。

关注的重点5:然后看下ReflectiveMethodInvocation作为一个Joinpoint的proceed方法的执行过程:

  1. public Object proceed() throws Throwable {
  2. // We start with an index of -1 and increment early.
  3. if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
  4. return invokeJoinpoint();
  5. }
  6.  
  7. Object interceptorOrInterceptionAdvice =
  8. this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
  9. if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
  10. // Evaluate dynamic method matcher here: static part will already have
  11. // been evaluated and found to match.
  12. InterceptorAndDynamicMethodMatcher dm =
  13. (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
  14. if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) {
  15. return dm.interceptor.invoke(this);
  16. }
  17. else {
  18. // Dynamic matching failed.
  19. // Skip this interceptor and invoke the next in the chain.
  20. return proceed();
  21. }
  22. }
  23. else {
  24. // It's an interceptor, so we just invoke it: The pointcut will have
  25. // been evaluated statically before this object was constructed.
  26. return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
  27. }
  28. }

  首先就是this.currentInterceptorIndex,它是ReflectiveMethodInvocation的一个属性,从-1开始:当currentInterceptorIndex达到this.interceptorsAndDynamicMethodMatchers.size() - 1时,拦截器链执行完毕了,就去执行目标对象的方法。invokeJoinpoint()方法就是上文我们所说的通过反射进行目标方法的调用。

  继续看,拿出一个interceptorOrInterceptionAdvice,判断它是不是InterceptorAndDynamicMethodMatcher类型,这个类型在获取拦截器链的时候遇见了,我们再次回顾下:

  1. for (Advisor advisor : config.getAdvisors()) {
  2. if (advisor instanceof PointcutAdvisor) {
  3. // Add it conditionally.
  4. PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;
  5. if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(targetClass)) {
  6. MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
  7. MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
  8. //重点在这里重点在这里重点在这里重点在这里重点在这里
  9. if (MethodMatchers.matches(mm, method, targetClass, hasIntroductions)) {
  10. if (mm.isRuntime()) {
  11. // Creating a new object instance in the getInterceptors() method
  12. // isn't a problem as we normally cache created chains.
  13. for (MethodInterceptor interceptor : interceptors) {
  14. interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm));
  15. }
  16. }
  17. else {
  18. interceptorList.addAll(Arrays.asList(interceptors));
  19. }
  20. }
  21. }
  22. }
  23. //略

  因为InterceptorAndDynamicMethodMatcher的MethodMatcher是可变的,所以在执行前仍要进行判断一次,符合的话就执行InterceptorAndDynamicMethodMatcher中所包含的MethodInterceptor。不符合的话跳过本次拦截器,继续执行下一个拦截器。 当拦截器是MethodInterceptor,则是执行这个拦截器。 然后我们来看下具体有哪些拦截器链,以及具体是怎样的执行过程: 
  我们会看到会有如下5个拦截器,依次是: ExposeInvocationInterceptor、MethodBeforeAdviceInterceptor、AspectJAfterAdvice、AspectJAroundAdvice、AspectJAfterThrowingAdvice。 
  即先执行后面的拦截器,但后面的拦截器执行过程中出现异常时才会发挥该拦截器的作用。继续执行后面的拦截器,发现已经没了,则终于轮到目标对象的方法了,目标方法执行完毕后,返回上一个proceed的嵌套即AspectJAfterThrowingAdvice的invoke方法,发现没有抛出异常,则继续返回到上一个proceed嵌套,即AspectJAroundAdvice,即我们自定义的doAround中这一行代码Object retVal = pjp.proceed()返回了,继续完成我们自定义的doAround函数,完成后再返回上一个proceed嵌套,来到AspectJAfterAdvice,则开始执行这个advice的处理工作,即我们自定义的doAfter方法。再返回上一个proceed嵌套,来到MethodBeforeAdviceInterceptor,发现已经执行完毕继续返回上一个嵌套来到ExposeInvocationInterceptor,继续完成余下的工作,至此整个拦截过程就分析完毕了。在此过程中一个重要的参数就是我们配置的拦截器的顺序,顺序不同时执行过程就不一样,我们可以通过在xml配置中指定,下面附上我画的拦截器链的执行流程图。

spring---aop(2)---Spring AOP的JDK动态代理的更多相关文章

  1. Spring AOP动态代理实现,解决Spring Boot中无法正常启用JDK动态代理的问题

    Spring AOP底层的动态代理实现有两种方式:一种是JDK动态代理,另一种是CGLib动态代理. JDK动态代理 JDK 1.3版本以后提供了动态代理,允许开发者在运行期创建接口的代理实例,而且只 ...

  2. aop学习总结一------使用jdk动态代理简单实现aop功能

    aop学习总结一------使用jdk动态代理实现aop功能 动态代理:不需要为目标对象编写静态代理类,通过第三方或jdk框架动态生成代理对象的字节码 Jdk动态代理(proxy):目标对象必须实现接 ...

  3. AOP的底层实现:JDK动态代理与Cglib动态代理

    转载自 https://www.cnblogs.com/ltfxy/p/9872870.html SpringAOP底层的实现原理: JDK动态代理:只能对实现了接口的类产生代理.(实现接口默认JDK ...

  4. Spring AOP详解 、 JDK动态代理、CGLib动态代理

    AOP是Aspect Oriented Programing的简称,面向切面编程.AOP适合于那些具有横切逻辑的应用:如性能监测,访问控制,事务管理以及日志记录.AOP将这些分散在各个业务逻辑中的代码 ...

  5. jdk动态代理与cglib代理、spring aop代理实现原理

    原创声明:本博客来源与本人另一博客[http://blog.csdn.net/liaohaojian/article/details/63683317]原创作品,绝非他处摘取 代理(proxy)的定义 ...

  6. jdk动态代理与cglib代理、spring aop代理实现原理解析

    原创声明:本博客来源为本人原创作品,绝非他处摘取,转摘请联系博主 代理(proxy)的定义:为某对象提供代理服务,拥有操作代理对象的功能,在某些情况下,当客户不想或者不能直接引用另一个对象,而代理对象 ...

  7. 何为代理?jdk动态代理与cglib代理、spring Aop代理原理浅析

    原创声明:本博客来源为本人原创作品,绝非他处摘取,转摘请联系博主 代理(proxy)的定义:为某对象提供代理服务,拥有操作代理对象的功能,在某些情况下,当客户不想或者不能直接引用另一个对象,而代理对象 ...

  8. Spring AOP高级——源码实现(3)AopProxy代理对象之JDK动态代理的创建过程

    spring-aop-4.3.7.RELEASE  在<Spring AOP高级——源码实现(1)动态代理技术>中介绍了两种动态代理技术,当然在Spring AOP中代理对象的生成也是运用 ...

  9. 【转载】Spring AOP详解 、 JDK动态代理、CGLib动态代理

    Spring AOP详解 . JDK动态代理.CGLib动态代理  原文地址:https://www.cnblogs.com/kukudelaomao/p/5897893.html AOP是Aspec ...

  10. spring AOP底层原理实现——jdk动态代理

    spring AOP底层原理实现——jdk动态代理

随机推荐

  1. Python 使用 Redis 操作

    1.redis简介 redis是一款开源免费的高性能key-value数据库,redis特点: 支持更多的数据类型:字符串(String).列表(List).哈希(Map).数字(Int).集合(Se ...

  2. mac idea内存溢出

    VM options: -mx2048m -XX:MaxPermSize=2048m -Drebel.spring_plugin=true -Drebel.hibernate_plugin=true

  3. 配置vuejs加载模拟数据

    [个人笔记,非技术博客] 1.使用前确保安装axios插件,vuejs官方推荐,当然使用其他插件也可以 2.配置dev-server.js var router = express.Router(); ...

  4. 一步步教你编写不可维护的 PHP 代码

    译者注:这是一篇很棒文章,使用有趣的叙述方式,从反面讲解了作为一个优秀的 PHP 工程师,有哪些事情是你不能做的.请注意哦,此篇文章罗列的行为,都是你要尽量避免的. 随着失业率越来越高,很多人意识到保 ...

  5. Owin 自定义中间件

    /// <summary> /// 自定义的中间件 /// </summary> public class CustomMiddleware : OwinMiddleware ...

  6. canvas 笔记整理

    canvas Retina 屏幕优化 /** * HiDPI Canvas Polyfill (1.0.9) * * Author: Jonathan D. Johnson (http://jonda ...

  7. LR检查点

    LR检查点   之前使用LoadRunner工具,一直认为,在开发脚本中检查点的设置是最容易的,直到现在,有一段时间没碰LR,今天录制了一段脚本,设置了文本检查点,回放脚本后,总是报错,描述一下我设置 ...

  8. 7-10 守卫棋盘 uva11214

    输入要给n*m的棋盘  均小于10   某些格子有标记  用最少的皇后  辐射到所有的标记 限时 6666ms 用IDA*    时间6000  尴尬. #include<bits/stdc++ ...

  9. docker动态绑定端口

    一.背景 在创建容器的时候,我们可以使用命令 docker container run -p host:container container-name 的方式来绑定端口,还可以使用docker-co ...

  10. Ionic Js九:列表操作

    列表是一个应用广泛在几乎所有移动app中的界面元素.ionList 和 ionItem 这两个指令还支持多种多样的交互模式,比如移除其中的某一项,拖动重新排序,滑动编辑等等. <ion-list ...