zoukankan      html  css  js  c++  java
  • Spring结合AspectJ的研究

    本文阐述以下内容:
    1、AspectJ是什么及使用方式
    2、Spring AOP和AspectJ的区别
    3、Spring结合AspectJ的使用方法和原理
    4、Spring注解方式使用AspectJ遇到的问题
    5、总结

    一、AspectJ是什么

    提到面向切面编程(AOP,Aspect Oriented Programming),大家首先想到的是Spring AOP,或许有人也会想到AspectJ,也有人搞不清楚这两者的区别。
    AOP是一种编程思想,AOP的作用是在不修改源程序的情况下修改源程序的动态执行流和静态属性。AspectJ是一种基于Java平台的面向切面编程的语言,兼容Java平台,可以无缝扩展,易学易用。干净的模块化横切关注点(也就是说单纯,基本上无侵入),如错误检查和处理,同步,上下文敏感的行为,性能优化,监控和记录,调试支持,多目标的协议。
    AspectJ有单独的语法,AspectJ项目的源文件经AspectJ Compiler(ajc)编译后生成完全兼容Java语法的Class文件,可以看出AspectJ可以在编译期将Advice功能织入连接点。AspectJ还可以在类加载期切面织入(Load Time Weaver,LTW),此时就需要在jvm启动参数配置 -javaagent:[path to aspectj-weaver.jar],这样在类加载时,AspectJ就可以将Advice功能织入连接点。比起编译期织入更方便,不需要用专用的ajc编译器编译,利用java.lang.Instrument包提供的工具在Java程序运行时动态修改系统中的Class类型。

    二、Spring AOP和AspectJ的区别

    Spring AOP是常用的AOP实现方式,使用方便,无需做项目工程之外的配置和操作,纯代码实现,深受广大开发人员的喜爱。Spring AOP是一个基于代理的AOP框架。运行时通过创建目标对象的代理类,对目标对象进行增强,主要使用JDK动态代理或CGLIB代理的方式。而AspectJ在运行时不做任何事情,在编译阶段或类加载阶段,Advice就织入切面到代码中了。这是和Spring AOP的本质区别。由Spring AOP使用的动态代理模式可知,通过实现目标的父接口或生成目标的子类来实现增强,因此只能对Spring管理范围内的bean的方法执行进行连接。AspectJ就要灵活很多,不仅能对方法执行进行连接,此外还支持方法调用、构造器调用、构造器执行、对象初始化、字段引用、字段赋值、类静态初始化、异常处理执行这些连接点。
    此外,在Spring AOP中,切面不适用于同一个类中调用的方法。当我们在同一个类中调用一个方法时,我们并没有调用Spring AOP提供的代理的方法。解决这个问题,可以在不同的beans中定义一个独立的方法,或者获取到本身的代理对象,或者使用AspectJ。
    性能方便,运行前织入比运行时织入快很多。Spring AOP是基于代理的框架,因此应用运行时会有目标类的代理对象生成。另外,每个切面还有一些方法调用,这会对性能造成影响。AspectJ不同于Spring AOP,是在应用执行前织入切面到代码中,没有额外的运行时开销。
    简而言之,选择很大程度上取决我们的需求:
    ●框架:如果应用程序不使用Spring框架,那么我们别无选择,只能放弃使用Spring AOP的想法,因为它无法管理任何超出spring容器范围的东西。但是,如果我们的应用程序完全是使用Spring框架创建的,那么我们可以使用Spring AOP,因为它很直接便于学习和应用。
    ●灵活性:鉴于有限的连接点支持,Spring AOP并不是一个完整的AOP解决方案,但它解决了程序员面临的最常见的问题。 如果我们想要深入挖掘并利用AOP达到其最大能力,并希望获得来自各种可用连接点的支持,那么AspectJ是最佳选择。
    ●性能:如果我们使用有限的切面,那么性能差异很小。但是,有时候应用程序有数万个切面的情况。在这种情况下,我们不希望使用运行时织入,所以最好选择AspectJ。已知AspectJ比Spring AOP快8到35倍。
    ●共同优点:这两个框架是完全兼容的。我们可以随时利用Spring AOP,并且仍然使用AspectJ来获得前者不支持的连接点。

     三、Spring结合AspectJ的使用方法和原理

    Spring AOP和AspectJ都可以使用AspectJ注解的方式来实现,实际上,Spring AOP为了遵循规范,或者和AspectJ保持兼容,借助了AspectJ的注解风格和AOP联盟定义的部分底层接口,不能想当然的认为Spring AOP是借助AspectJ来实现的,在原理上Spring AOP和AspectJ没有关系。
    Spring AOP如何使用就不细说了,下面说下Spring结合AspectJ注解方式的使用。

    切面声明:

     1 @Aspect
     2 public class MyAspect {
     3 
     4     @Pointcut("execution(* com.mzsea.spring.service.*.*(..))")
     5     private void pointCut() {
     6 
     7     }
     8 
     9     @Around("pointCut()")
    10     public Object myAround(ProceedingJoinPoint joinPoint) throws Throwable {
    11         System.out.println("before");
    12         Object obj = joinPoint.proceed();
    13         System.out.println("after");
    14         return obj;
    15     }

    业务逻辑:

    1 @Component
    2 public class UserServiceImpl {
    3 
    4     public void add() {
    5         System.out.println("hello user!");
    6 }

    Spring配置:

    1 <context:load-time-weaver/>
    2 <context:component-scan base-package="com.mzsea.spring"/>

     AspectJ配置(class目录下META-INF/aop.xml):

    1 <aspectj>
    2     <weaver options="-verbose -debug -showWeaveInfo">
    3         <include within="com.mzsea.spring..*"/>
    4     </weaver>
    5     <aspects>
    6         <aspect name="com.mzsea.spring.aspectj.MyAspect"/>
    7     </aspects>
    8 </aspectj>

    Main方法:

    1     public static void main(String[] args) {
    2         ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application.xml");
    3         UserServiceImpl userServiceImpl = applicationContext.getBean(UserServiceImpl.class);
    4         userServiceImpl.add();
    5     }

    运行结果:

    before
    hello user!
    after

    以上就是一个简单的AspectJ应用,和Spring AOP相比,配置略有不同。

    <aop:aspectj-autoproxy/>换成<context:load-time-weaver/>
    还多了META-INF/aop.xml配置,并且在运行时需要指定java agent参数-javaagent:spring-instrument-{version}.jar

    @Aspect注解的类并不在spring的bean管辖之内,完全由AspectJ使用。Spring只是整合了AspectJ的入口,让AspectJ在类加载时改变Class字节码,然后就与AspectJ无关了。

    接下来分析下spring是如何结合AspectJ的。

    Java引入了java.lang.Instrument包,该包提供了一些工具帮助开发人员在Java程序运行时,动态修改系统中的Class类型,并不需要自定义类加载器,使用该软件包的一个关键组件就是Java agent。Java agent的使用规范就是在指定的jar包内的MANIFEST.MF文件指定Premain-Class项,Premain-Class指定的那个类必须实现premain()方法。

    打开spring-instrument.jar,找到Premain-Class指定的类,代码如下:

    1     private static volatile Instrumentation instrumentation;
    2     public static void premain(String agentArgs, Instrumentation inst) {
    3         instrumentation = inst;
    4     }

    发现超级简单啊,就是把Instrumentation参数赋值给静态变量了,一看感觉没起任何作用啊,怎么可能就这么简单就实现了,确实如你所想,spring在下一盘大棋,这边只是暴露出Instrumentation,后面如何对Instrumentation操作就是关键了。

    Spring启动过程中,会解析<context:load-time-weaver/>这个配置。
    我们知道这个xml元素会由ContextNamespaceHandler处理,打开此类:

     1     public void init() {
     2         registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());
     3         registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());
     4         registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
     5         registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
     6         registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());
     7         registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
     8         registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());
     9         registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
    10     }

    果然看到load-time-weaver对应的解析器,LoadTimeWeaverBeanDefinitionParser。
    解析器通过配置属性或者自动识别有无META-INF/aop.xml来判断是否开启AspectJWeaving,开启了就会注册相关的BeanDefinition。这里会注册
    AspectJWeavingEnabler、DefaultContextLoadTimeWeaver这2个BeanDefinition。

    Spring配置xml的解析在著名的refresh()方法的obtainFreshBeanFactory()阶段就已经完成,此时xml中定义的配置都已经转换成各种BeanDefinition对象存储在BeanFactory中,接着在prepareBeanFactory()中有这样的代码:

    1    if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
    2        beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
    3        // Set a temporary ClassLoader for type matching.
    4        beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
    5    }

    意思就是如果存在名为“loadTimeWeaver”的bean,则注册LoadTimeWeaverAwareProcessor这个BeanPostProcessor,这个很好理解,就是实现了LoadTimeWeaverAware接口的bean会自动注入“loadTimeWeaver”这个bean,和其他的Aware接口一个道理。setTempClassLoader这个操作是创建一个临时ClassLoader,ContextTypeMatchClassLoader,从名字大概能看出是上下文类型匹配类加载器的意思,但是到底有何用意暂时还不得而知。暂且留下疑问。

    refresh()在执行完prepareBeanFactory()后,接着执行postProcessBeanFactory()、invokeBeanFactoryPostProcessors()、registerBeanPostProcessors()等关键操作,最后阶段还会初始化不是懒加载的单例bean。
    invokeBeanFactoryPostProcessors()是执行BeanFactory后处理器,此时BeanFactory已经加载结束,正式触发BeanFactory后处理器的时候,这里是非常重要的扩展点,包括Spring本身,都利用这个扩展点干很多事。

    回看AspectJWeavingEnabler,其实就是BeanFactoryPostProcessor的实现类之一,找到postProcessBeanFactory方法:

     1 public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
     2         enableAspectJWeaving(this.loadTimeWeaver, this.beanClassLoader);
     3     }
     4 public static void enableAspectJWeaving(LoadTimeWeaver weaverToUse, ClassLoader beanClassLoader) {
     5         if (weaverToUse == null) {
     6             if (InstrumentationLoadTimeWeaver.isInstrumentationAvailable()) {
     7                 weaverToUse = new InstrumentationLoadTimeWeaver(beanClassLoader);
     8             }
     9             else {
    10                 throw new IllegalStateException("No LoadTimeWeaver available");
    11             }
    12         }
    13         weaverToUse.addTransformer(
    14                 new AspectJClassBypassingClassFileTransformer(new ClassPreProcessorAgentAdapter()));
    15     }

    在执行postProcessBeanFactory时,作为bean的AspectJWeavingEnabler已经实例化了,已经注入loadTimeWeaver为DefaultContextLoadTimeWeaver,DefaultContextLoadTimeWeaver中会根据不同的应用环境(Tomcat、GlassFish、JBoss、WebSphere、WebLogic、默认)创建相应的LoadTimeWeaver,因为不同的应用容器都实现了自己的类加载器,注册transformer的方式各有差异,默认环境中使用的类加载器是sun.misc.Launcher.AppClassLoader,DefaultContextLoadTimeWeaver包装了实际的LoadTimeWeaver,是装饰器模式的应用,LoadTimeWeaver提供统一的接口屏蔽了不同类加载器注册transformer的差异,实现一致处理。这里实际创建了InstrumentationLoadTimeWeaver,上述代码13行调用了addTransformer(),最终调用了Instrumentation.addTransformer(),参数为通过委托模式包装过的AspectJ的ClassPreProcessorAgentAdapter,至此,这就是通过Spring整合AspectJ的过程,至于具体的类加载阶段的处理则由AspectJ接管。

    可见Spring通过BeanFactoryPostProcessor对AspectJ进行了整合,在这个阶段,用户自定义的Bean还没有初始化,对应的Class也大概率未加载,整合后,加载Class大部分情况下就会被AspectJ拦截,根据配置进行字节码修改,实现切面增强。如果需要增强的Class在Spring和AspectJ整合之前就已经加载过了,根据ClassLoader的加载规则可知,对于同一个ClassLoader,同一Class很少情况下会加载两次(Class被gc回收的条件苛刻),此时需要增强的Class就错过了切面织入过程,AOP就失效了。所以需要尽早的对AspectJ进行了整合,BeanFactoryPostProcessor是Spring初始化过程中比较靠前的扩展点,AspectJ在此整合不失为一个合理的时机。

    四、Spring注解方式使用AspectJ遇到的问题

    从上节中了解的Spring结合AspectJ的原理,在使用时,更倾向于简单的配置。
    现在流行的Spring Boot项目结构,提倡简洁配置,舍弃xml,Spring Boot针对一些常见配置做了默认处理,用户无需配置过多,结合Spring提供的xml与注解对应关系,使应用结构大为简化。
    <context:load-time-weaver/>这个配置,与之对应的注解配置为@EnableLoadTimeWeaving,只要在@Configuration声明的类上配置上@EnableLoadTimeWeaving,应用启动过程中就可以解析注解,生成对应的BeanDefinition。

    ConfigurationClassPostProcessor这个类为识别Spring Class注解的入口,@Configuration、@ComponentScans、@ImportResource、@Import等等这些注解,都由ConfigurationClassPostProcessor负责解析,生成BeanDefinition,具体的解析过程,可以跟踪postProcessBeanFactory观得全貌。

    查看@EnableLoadTimeWeaving代码:

    1 @Import(LoadTimeWeavingConfiguration.class)
    2 public @interface EnableLoadTimeWeaving {
    3 4 }

    发现对这个注解又声明了@Import注解,@Import的作用就和xml配置中的import标签类似,用于加载指定参数中的bean配置,查看LoadTimeWeavingConfiguration代码:

     1 @Bean(name = ConfigurableApplicationContext.LOAD_TIME_WEAVER_BEAN_NAME)
     2 @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
     3 public LoadTimeWeaver loadTimeWeaver() {
     4    LoadTimeWeaver loadTimeWeaver = null;
     5 
     6    if (this.ltwConfigurer != null) {
     7       // The user has provided a custom LoadTimeWeaver instance
     8       loadTimeWeaver = this.ltwConfigurer.getLoadTimeWeaver();
     9    }
    10 
    11    if (loadTimeWeaver == null) {
    12       // No custom LoadTimeWeaver provided -> fall back to the default
    13       loadTimeWeaver = new DefaultContextLoadTimeWeaver(this.beanClassLoader);
    14    }
    15 
    16    AspectJWeaving aspectJWeaving = this.enableLTW.getEnum("aspectjWeaving");
    17    switch (aspectJWeaving) {
    18       case DISABLED:
    19          // AJ weaving is disabled -> do nothing
    20          break;
    21       case AUTODETECT:
    22          if (this.beanClassLoader.getResource(AspectJWeavingEnabler.ASPECTJ_AOP_XML_RESOURCE) == null) {
    23             // No aop.xml present on the classpath -> treat as 'disabled'
    24             break;
    25          }
    26          // aop.xml is present on the classpath -> enable
    27          AspectJWeavingEnabler.enableAspectJWeaving(loadTimeWeaver, this.beanClassLoader);
    28          break;
    29       case ENABLED:
    30          AspectJWeavingEnabler.enableAspectJWeaving(loadTimeWeaver, this.beanClassLoader);
    31          break;
    32    }
    33 
    34    return loadTimeWeaver;
    35 }

    看到了熟悉的“loadTimeWeaver”配置。
    loadTimeWeaver实例化过程中,从代码16行开始,通过判断配置的aspectJWeaving属性来决定启用AspectJWeaving,代码27行,和前一小节提及的AspectJWeavingEnabler中postProcessBeanFactory调用了同样的方法,由此可见,xml配置方式和注解方式最后都殊途同归,只不过开始阶段的解析不一样。

    实践,舍去xml,换上注解:

    1 @Configuration
    2 @EnableLoadTimeWeaving
    3 public class AppConfig {
    4 
    5 }

    启动项目,预计结果应该和之前一样,可是事与愿违,发现切面没被织入,业务代码没有增强。
    配置仔细推敲了好几遍,还是不行,为什么xml方式配置就可以,注解方式就失效了呢。

    猜想,可能是需要被增强的Class在AspectJ生效前被加载了!
    打开JVM参数-verbose,控制台会打印class加载信息,观察class加载情况。
    打开AspectJ调试参数:<weaver options="-verbose -debug -showWeaveInfo">
    观察两种配置下的UserServiceImpl.class加载位置。

    AspectJ调试日志:

    [AppClassLoader@18b4aac2] info AspectJ Weaver Version 1.8.13 built on Wednesday Nov 15, 2017 at 19:26:44 GMT
    [AppClassLoader@18b4aac2] info register classloader sun.misc.Launcher$AppClassLoader@18b4aac2
    [AppClassLoader@18b4aac2] info using configuration /D:/Workspaces/eclipse/aspectj/target/classes/META-INF/aop.xml

    意外的是,xml配置和注解配置在AspectJ加载之前都加载了UserServiceImpl.class,但是加载日志有所区别:

    Xml配置下UserServiceImpl.class加载:

    [Loaded com.mzsea.spring.service.UserServiceImpl from __JVM_DefineClass__]

    注解配置下UserServiceImpl.class加载:

    [Loaded com.mzsea.spring.service.UserServiceImpl from file:/D:/Workspaces/eclipse/aspectj/target/classes/]

    比较发现一个是从JVM内部加载,一个是从class文件加载,这可能就是产生问题所在。

    反复调试加载位置上下文,发现在调用ConfigurationClassPostProcessor.postProcessBeanFactory()中有加载UserServiceImpl.class,问题浮出水面,因为同一个ClassLoader对同一个Class只加载一次的规则,还没有执行到LoadTimeWeaver相关的代码,UserServiceImpl.class就已经被加载了,后面AspectJ加载后,自然不会再对UserServiceImpl.class拦截增强。

    虽然两种不同的配置都有加载UserServiceImpl.class,但是从日志上看就是有区别的。

    深入UserServiceImpl.class初始加载的代码:org.springframework.beans.factory.support.AbstractBeanFactory#doResolveBeanClass

     1 private Class<?> doResolveBeanClass(RootBeanDefinition mbd, Class<?>... typesToMatch)
     2       throws ClassNotFoundException {
     3 
     4    ClassLoader beanClassLoader = getBeanClassLoader();
     5    ClassLoader classLoaderToUse = beanClassLoader;
     6    if (!ObjectUtils.isEmpty(typesToMatch)) {
     7       // When just doing type checks (i.e. not creating an actual instance yet),
     8       // use the specified temporary class loader (e.g. in a weaving scenario).
     9       ClassLoader tempClassLoader = getTempClassLoader();
    10       if (tempClassLoader != null) {
    11          classLoaderToUse = tempClassLoader;
    12          if (tempClassLoader instanceof DecoratingClassLoader) {
    13             DecoratingClassLoader dcl = (DecoratingClassLoader) tempClassLoader;
    14             for (Class<?> typeToMatch : typesToMatch) {
    15                dcl.excludeClass(typeToMatch.getName());
    16             }
    17          }
    18       }
    19    }
    20    String className = mbd.getBeanClassName();
    21    if (className != null) {
    22       Object evaluated = evaluateBeanDefinitionString(className, mbd);
    23       if (!className.equals(evaluated)) {
    24          // A dynamically resolved expression, supported as of 4.2...
    25          if (evaluated instanceof Class) {
    26             return (Class<?>) evaluated;
    27          }
    28          else if (evaluated instanceof String) {
    29             return ClassUtils.forName((String) evaluated, classLoaderToUse);
    30          }
    31          else {
    32             throw new IllegalStateException("Invalid class name expression result: " + evaluated);
    33          }
    34       }
    35       // When resolving against a temporary class loader, exit early in order
    36       // to avoid storing the resolved Class in the bean definition.
    37       if (classLoaderToUse != beanClassLoader) {
    38          return ClassUtils.forName(className, classLoaderToUse);
    39       }
    40    }
    41    return mbd.resolveBeanClass(beanClassLoader);
    42 }

    发现此处在对class加载时会选择不同的ClassLoader,beanClassLoader和tempClassLoader。getTempClassLoader()即是上文
    beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
    如果tempClassLoader为null,就使用beanClassLoader,beanClassLoader显然是加载bean class用的,如果没有tempClassLoader,bean class就被提前加载了,导致AspectJ失效。

    查看方法栈,发现调用doResolveBeanClass的外层方法有:

    1 AbstractBeanFactory#isFactoryBean()
    2 ListableBeanFactory#getBeanNamesForType()

    可以看到,在spring启动过程中,经常需要通过判定bean的类型去做处理,如何知道bean的类型,就需要再加bean对应的Class,这就是问题所在。

    启动过程中,prepareBeanFactory()中判断beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)的目的就是如果存在“loadTimeWeaver”,那么就设置tempClassLoader,而使用注解方式配置,使“loadTimeWeaver”的定义延迟到invokeBeanFactoryPostProcessors()中了,虽然spring也在invokeBeanFactoryPostProcessors()中进行了补救:

     1 protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
     2    PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
     3 
     4    // Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
     5    // (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)
     6    if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
     7       beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
     8       beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
     9    }
    10 }

    第一行调用中处理了各种注解,过程中调用了isFactoryBean()、getBeanNamesForType()等判断类型的方法,由于TempClassLoader为null,使用beanClassLoader加载了大部分bean class,虽然生成了“loadTimeWeaver”,所以接下来的setTempClassLoader也没多大作用了。

    那如果用注解配置下是不是及时检查“loadTimeWeaver”设置TempClassLoader就可以了呢?

    AspectJWeavingEnabler#enableAspectJWeaving()这是结合的关键,这个方法不执行,AspectJ就不起作用。xml配置的时候,这个方法在postProcessBeanFactory就执行了,还算靠前;但是注解配置的时候,这个方法却延迟到“loadTimeWeaver”实例化时进行,bean实例化已经是spring启动过程的尾声了,在refresh()方法末尾的finishBeanFactoryInitialization()中才进行,而且bean的实例化先后顺序不固定,有可能需要织入增强逻辑的bean先实例化,而“loadTimeWeaver”后实例化,此时AspectJ是彻底不起作用了。

    总之,要使AspectJ起作用,必须尽早发现“loadTimeWeaver”,setTempClassLoader,尽早加载AspectJ,postProcessBeanFactory()是比较早的机会,但是也不能百分百避免bean class在AspectJ前被beanClassLoader加载,此处为Spring的一个bug,修复需要重新调整代码结构,有空会提交PR。

    五、总结

    通过分析,Spring将AspectJ融入体系不是容易的事,绕了很多路,稍不注意AspectJ就不起作用。使用另一个ClassLoader去加载Class用于做bean的类型的判定,而不影响本身bean class的加载,AspectJ的加载点很重要,尽早加载越好。

    作者:hackem
             
    本文版权归原作者所有,欢迎转载,但必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利。
  • 相关阅读:
    跨域资源共享 CORS 详解
    C# 每月第一天和最后一天
    jexus http to https
    ASP.NET MVC 路由学习
    jexus
    centos7 添加开机启动项
    jexus docker
    HTTP2.0新特性
    jexus配置支持Owin
    Autofac Named命名和Key Service服务
  • 原文地址:https://www.cnblogs.com/hackem/p/9718311.html
Copyright © 2011-2022 走看看