Spring5源码13-AOP源码分析(上)

SpringAOP中@EnableAspectJAutoProxy注解的作用 理解@EnableAspectJAutoProxy注解的作用 阅读详情

1. Aop介绍及使用

Aop 即面向切面编程,而 Aspect 是Aop 思想的一种实现。并不是所有的AOP框架都相同,它们在连接点模型上可能有强弱之分,有些允许在字段修饰符级别的应用通知,有些只支持方法调用相关的连接点。需要注意的是 Spring 只支持方法级别的连接点。

Spring 提供了4种类型的AOP支持:

  • 基于代理的经典Spring Aop
  • 纯Pojo切面
  • @Aspect注解驱动的切面
  • 注入式的Aspectj的切面

前三种都是Spring AOP 实现的变体,Spring AOP 构建在动态代理之上,因此Spring 对Aop的支持局限于方法拦截。

本文中会介绍 经典的的Aop使用(ProxyFactoryBean)和 @Aspect注解驱动的切面。

二者的区别个人理解在于:AspectJ的代理模式:解析@Aspect注解的类,根据方法上的不同注解,来动态封装成Advisor (顾问),里面包含了方法对应的Advice 和指定的切点(这里的切点方法匹配是根据逻辑表达式来,而传统的SpringAop需要自己写判断逻辑,其实也可以写一个逻辑表达式判断,所以这里是一样的,只是实现方式的不同而已)

Spring Aop 和 AspectJ 的关系: AspectJ 是一套AOP框架,是对java语言语法和语义的扩展,所以他自己提供了一套关键字,这也就是说,如果在没有安装 AspectJ的情况下,是无法使用 AspectJ 的。这里需要注意的是,在 Spring框架中使用的 @Aspect 注解实现的 Aop 功能并不是上面所说的AspectJ 框架, 在Spring中使用 @Aspect 注解实现的AOP 功能,其底层实现还是 Spring Aop。

1.1 经典Spring Aop

1.1.1 基本释义

经典的 Spring Aop 中有几个关键类

  • Advisor : 顾问,顾问是Spring提供的另一种切面。可以完成更为复杂的切面织入功能。PointcutAdvisor是顾问的一种,可以指定具体的切入点。顾问将通知进行了包装,会根据不同的通知 类型,在不同的时间点,将切面织入到不同的切入点。通知和顾问都是切面的实现方式增强点, 包含Advice和Pointcut。个人认为是 Spring AOP完成增强动作的最小单元。
  • Advice : 通知,通知是Spring提供的一种切面(Aspect)。但是其功能过于简单,只能讲切面织入到目标类的所有目标方法中,无法完成讲切面织入到指定目标方法中。通知实际上使用具体的增强操作,即切面织入之后的实际操作。 Pointcut :切点信息, 这个主要是用来确定切入点在那,即在那切入。

Advisor两个子接口PointcutAdvisor、IntroductionAdvisor :

IntroductionAdvisor与PointcutAdvisor 最本质上的区别就是,IntroductionAdvisor只能应用于类级别的拦截,只能使用Introduction型的Advice。而不能像PointcutAdvisor那样,可以使用任何类型的Pointcut,以及几乎任何类型的Advice。

PointCutAdvisor接口 比较常用的两个实现类:

  • 根据切入点(主业务方法)名称织入切面 :NameMatchMethodPointCutAdvisor
  • 根据自定义的正则表达式织入切面 :RegexpMethodPointoutAdvisor

1.1.2 简单Demo

声明两个通知类型 。一个是 MethodBeforeAdvice ,一个是 AfterReturningAdvice,见名知意,一个在方法执行前调用,一个在方法执行后调用

@Component
public class DemoAfterReturnAdvice implements AfterReturningAdvice {
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("DemoAfterReturnAdvice.afterReturning");
    }
}
@Component
public class DemoBeforeAdvice implements MethodBeforeAdvice {

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("DemoBeforeAdvice.before");
    }
}

被代理的目标类 DemoController:

public class DemoController {

    public void hello(String msg) {
        System.out.println("hello " + msg);
    }
}

配置了将代理对象注入,这里可以看到注入的是ProxyFactoryBean 。我们知道FactoryBean会将 getObject 方法的返回值作为结果注入到Spring容器中。这里不难猜测,ProxyFactoryBean 的getObject 方法中必定做了代理。

@Configuration
public class AopConfig {

    @Bean("demoController")
    public ProxyFactoryBean proxyFactoryBean() {
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
        // 设置代理的目标类
        proxyFactoryBean.setTarget(new DemoController());
        // 设置通知拦截器
        proxyFactoryBean.setInterceptorNames("demoAfterReturnAdvice", "demoBeforeAdvice");
        return proxyFactoryBean;
    }
}

我们来调用试试:

@SpringBootApplication
public class AopDemoApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(AopDemoApplication.class, args);
        DemoController demoController = (DemoController) run.getBean("demoController");
        demoController.hello("123");
    }
}

输出结果如下: 

综上:可以看到 经典Spring AOP 起了作用。 核心逻辑就是在 ProxyFactoryBean 中的getObject方法中。

 

@Override
@Nullable
public Object getObject() throws BeansException {
        // 初始化连接器链路
        initializeAdvisorChain();
        if (isSingleton()) {
                // 获取代理类
                return getSingletonInstance();
        }
        else {
                if (this.targetName == null) {
                        logger.info("Using non-singleton proxies with singleton targets is often undesirable. " +
                                        "Enable prototype proxies by setting the 'targetName' property.");
                }
                return newPrototypeInstance();
        }
}

主要步骤:

  1. 在getObject时会调用 initializeAdvisorChain() 根据InterceptorNames来初始化拦截器。
  2. 将拦截器包装成Advisor。(目前看来, Spring AOP一个增强功能最基本的实现单元就是Advisor)
  3. 将Advisor保存到this.advisors 集合中
  4. newPrototypeInstance();创建代理对象。这里的创建逻辑和@Aspect注解的AOP 实现逻辑基本相同(毕竟@Aspect使用的就是Spring AOP实现的) 5.根据某些条件选择cglib(CglibAopProxy)或者jdk(JdkDynamicAopProxy)代理方式。

关于详细的代码解读,后续会有文章进行解读。

1.2 @Aspect注解驱动的切面

  • 切面(Aspect) :官方的抽象定义为“一个关注点的模块化,这个关注点可能会横切多个对象”。
  • 连接点(Joinpoint) :程序执行过程中的某一行为。
  • 通知(Advice) :“切面”对于某个“连接点”所产生的动作。
  • 切入点(Pointcut) :匹配连接点的断言,在AOP中通知和一个切入点表达式关联。
  • 目标对象(Target Object) :被一个或者多个切面所通知的对象。 AOP代理(AOP Proxy) 在Spring AOP中有两种代理方式,JDK动态代理和CGLIB代理。

1.2.1 定义切点

Spring 借助 AspectJ 的切点表达式语言来定义 Spring 切面

下面是用来定义切点的的描述符:

AspectJ指示器 描述
arg() 限制连接点匹配参数为指定类型的执行方法
@args() 限制连接点匹配参数由指定注解标注的执行方法
execution() 用于匹配是连接点的执行方法
this() 限制连接点匹配 AOP代理的bean引用为指定类型的类
target 限制连接点匹配的目标对象为指定类型的类
@target() 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解
within() 限制连接点匹配指定的类型
@within() 限制连接匹配指定注解所标注的类型(当使用 Spring Aop 时,方法定义在由指定的注解所标注的类里)
@annotation 限定匹配带有指定注解的连接点
bean() 限定 bean 的id

上述指示器中,只有 execution() 指示器是实际匹配执行的,其余都是限制匹配的。

execution (* com.kingfish.AopTest.test(..) && within(com.kingfish.AopTest.*))

其中,使用execution() 执行器,选择了 com.kingfish.AopTest.test() 作为切点, * 代表这个方法可以返回任意类型。 .. 代表 这个方法可以使用任意参数。&& 代表与,也可以使用 and(与之类似的还有 || 代表或(or)、!代表非( not )。注意在xml配置中因为 & 具有其他含义,所以可以使用and代替&&) ,within() 代表 连接的一个操作。 within(com.kingfish.AopTest.*) 代表 com.kingfish.AopTest 类 的任意方法被调用。即这个切点的整个意义是, com.kingfish.AopTest.test方法被调用(这个方法可以传递任意参数,也可返回任意类型的返回值) 并且 com.kingfish.AopTest的任意方法被调用。

execution (* com.kingfish.AopTest.test(..) && bean('aop'))

这个切点的整个意义是, com.kingfish.AopTest.test方法被调用(这个方法可以传递任意参数,也可返回任意类型的返回值)但限定bean的Id为 aop。

1.2.2 定义切面

Spring 使用 AspectJ 注解来声明通知方法:

注解 通知
@After 通知方法会在目标方法返回或抛出异常后调用
@AfterReturning 通知方法会在目标方法返回后调用
@AfterThrowing 通知方法会在目标方法抛出异常后调用
@Around 通知方法会将目标方法封装起来,环绕通知方式(后续实例中讲解)
@Before 通知方法会在目标方法调用之前执行

1.3 代码实践

上面说的很混乱,如果没有代码对没有接触的过的人不好理解,这里通过代码来进行进一步的分析.

引入依赖:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.5</version>
</dependency>

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.9</version>
</dependency>

实体类:

@Component  //切面存在的化就会返回代理对象
public class HelloService {

   public HelloService(){
      System.out.println("....");
   }

   public String sayHello(String name){
      String result = "你好:"+name;
      System.out.println(result);
      int length = name.length();
      return result + "---" + length;
   }
}

编写切面类并定义切点:

/**
 * Spring5以后顺序就一切正常
 * 正常:前置通知===目标方法===返回通知===后置通知
 * 异常: 前置通知===目标方法===异常通知===后置通知
 * try{
 *     前置通知
 *     目标方法的执行
 *     返回通知
 * }catch(){
 *     异常通知
 * }finally{
 *     后置通知
 * }
 */
@Component  //切面也是容器中的组件
@Aspect //说明这是切面
public class LogAspect {

   public LogAspect(){
      System.out.println("LogAspect...");
   }

   //前置通知  增强方法/增强器
   @Before("execution(* com.hsf.spring.aop.HelloService.sayHello(..))")
   public void logStart(JoinPoint joinPoint){
      String name = joinPoint.getSignature().getName();
      System.out.println("logStart()==>"+name+"....【args: "+ Arrays.asList(joinPoint.getArgs()) +"】");
   }

   //返回通知
   @AfterReturning(value = "execution(* com.hsf.spring.aop.HelloService.sayHello(..))",returning = "result")
   public void logReturn(JoinPoint joinPoint,Object result){
      String name = joinPoint.getSignature().getName();
      System.out.println("logReturn()==>"+name+"....【args: "+ Arrays.asList(joinPoint.getArgs()) +"】【result: "+result+"】");
   }


   //后置通知
   @After("execution(* com.hsf.spring.aop.HelloService.sayHello(..))")
   public void logEnd(JoinPoint joinPoint){
      String name = joinPoint.getSignature().getName();
      System.out.println("logEnd()==>"+name+"....【args: "+ Arrays.asList(joinPoint.getArgs()) +"】");
   }


   //异常
   @AfterThrowing(value = "execution(* com.hsf.spring.aop.HelloService.sayHello(..))",throwing = "e")
   public void logError(JoinPoint joinPoint,Exception e){
      String name = joinPoint.getSignature().getName();
      System.out.println("logError()==>"+name+"....【args: "+ Arrays.asList(joinPoint.getArgs()) +"】【exception: "+e+"】");
   }
}

配置类中 开启 AspectJ 代理:

@Configuration
//@Import({Person.class}) // 导入某个bean,默认使用无参构造器创建对象
//@Import(value = {MainConfig.MyImportBeanDefinitionRegistrar.class})
@ComponentScan("com.hsf.spring")  // 默认扫描当前路径下的bean
@EnableAspectJAutoProxy
public class MainConfig {
   @Bean
   public Person getPerson() {
      Person person = new Person();
      person.setName("张三");
      return person;
   }
}

测试类:

public class AnnotationMainTest {
   public static void main(String[] args) {

      AnnotationConfigApplicationContext context =
            new AnnotationConfigApplicationContext(MainConfig.class);

      //AOP,原理测试
      HelloService helloService = context.getBean(HelloService.class);
      helloService.sayHello("zhangsan");
    }
}

运行结果如下:

logStart()==>sayHello....【args: [zhangsan]】
你好:zhangsan
logReturn()==>sayHello....【args: [zhangsan]】【result: 你好:zhangsan---8】
logEnd()==>sayHello....【args: [zhangsan]】

环绕通知的使用:在上面的Demo基础上,我们来看看环绕通知的使用。我们需要继续编写 AopDemo类。仅需在 AopDemo 中添加如下方法

@Around("execution(* com.hsf.spring.aop.HelloService.sayHello(..))")
public void around(ProceedingJoinPoint joinPoint) {
   System.out.println("around start");
   try {
      System.out.println("around before");
      joinPoint.proceed();    // 放行切点的方法,不放行则会阻塞调用
      System.out.println("around after");
   } catch (Throwable throwable) {
      throwable.printStackTrace();
      System.out.println("around throwable");
   }

}

测试结果可以看到如下:

around start
around before
logStart()==>sayHello....【args: [zhangsan]】
你好:zhangsan
logReturn()==>sayHello....【args: [zhangsan]】【result: 你好:zhangsan---8】
logEnd()==>sayHello....【args: [zhangsan]】
around after

1.4 MethodInterceptor和HandlerInterceptor区别:

  • HandlerInterceptor :是Spring mvc提供的拦截器,这种拦截器的生效时机实在DispatcherServlet分发请求时生效。并非是依赖于SpringAOP功能。正因如此,是只能拦截Controller层的方法请求。使用时需要重写WebMvcConfigurerAdapter.addInterceptors方法,来添加指定的拦截器。

  • org.springframework.cglib.proxy.MethodInterceptor :这个是 Cglib 进行代理时所使用的拦截器。

  • org.aopalliance.intercept.MethodInterceptor :就是利用Spring AOP生成的拦截器。所以实际上MethodInterceptor的实现也就是Spring Aop的实现,和之前写的ProxyFactoryBean的用法相同,可以拦截所有层面的方法。其实现也继承了 Advice 接口,结构如下:

2. AOP注入了那些组件?@EnableAspectJAutoProxy

在使用AspectJ AOP 功能时,我们需要使用注解 @EnableAspectJAutoProxy 来开启Aop 功能。那么我们的分析入口自然是从这个注解开始。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
   // 是否代理目标对象:即是否使用cglib 代理
   boolean proxyTargetClass() default false;
   
   // 是否暴露代理对象
   boolean exposeProxy() default false;
}

 

分析了这么久的源码,从上面我们可以看到 @EnableAspectJAutoProxy 注解 中使用了 @Import(AspectJAutoProxyRegistrar.class) 注解引入了AspectJAutoProxyRegistrar 类,因此我们下面来看看 AspectJAutoProxyRegistrar 类的实现。其继承图:

AspectJAutoProxyRegistrar 实现了 ImportBeanDefinitionRegistrar 接口,那么我们自然要看看他的registerBeanDefinitions 方法了,registerBeanDefinitions 方法的作用是在Spring进入下一步动作之前可以添加BeanDefinition,而Spring Aop 在这里将会将自动代理创建器 AbstractAutoProxyCreator添加到Spring容器中,AbstractAutoProxyCreator 是Spring 实现Aop的核心类。(Spring 在 ConfigurationClassPostProcessor 中完成了对 ImportBeanDefinitionRegistrar 接口的处理,主要功能还是将BeanDefinition保存,等待Spring解析加载到容器中。

我们直接看核心registerBeanDefinitions方法:

class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {

   @Override
   public void registerBeanDefinitions(
         AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

      // 如有必要,注册Aspect J注释自动代理创建器。这里注册的自动代理创建器Aop 实现的核心
      // 这里之所以说如有必要,是因为在调用该方法时,容器中可能已经创建了一个自动代理创建器,
      // 如果这个自动代理创建器优先级更高或者与当前需要创建的自动代理创建器是同一类型,则不需要创建。

      // 注册切面的基于注解的自动代理创建器
      AopConfigUtil
Spring5源码13-AOP源码分析(下)创建代理和执行过程 在方法中:首先处理equals 和hashcode 等方法。随后调用方法将获取到拦截器。因为Jdk动态代理在调用增强方法时,是通过方法来调用,但对于来说,只有和实现了接口,其余的并没有实现接口。所以在中,将Advisor中的 Advice 都封装成,以便后面方法增强调用。随后封装成,其实就是一个简单的封装,以便后面处理。关于// MethodInterceptor 方法拦截器,这里保存了针对于该方法的增强实现// 方法匹配器,用来匹配当前拦截器是否适用于当前方法}}复制代码至此,... 阅读详情

相关推荐

19-Spring AOP源码分析-代理对象调用目标方法

【代码】19-Spring AOP源码分析-代理对象调用目标方法。

Java架狗师知识框架速查表 899

15-Spring架构源码分析-Spring代理与AOP

日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,

Java架狗师知识框架速查表 253

18-Spring AOP源码分析-AOP与BeanPostProcessor处理器

1:>org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#postProcessAfterInitialization源码分析*

Java架狗师知识框架速查表 433

1-Spring架构源码分析-Spring源码搭建

spring 5.0源码环境搭建

Java架狗师知识框架速查表 386

17-Spring AOP源码分析-AnnotationAwareAspectJAutoProxyCreator

3)所以我们现在可以分析一下AnnotationAwareAspectJAutoProxyCreator 他是什么一个鬼?根据上诉类图1)我们***发现了AnnotationAwareAspectJAutoProxyCreator 有实现了**Aware接口的特性(BeanFactoryAware)2)还发现了*AnnotationAwareAspectJAutoProxyCreator 实现了BeanPostProcessor接口(后置处理器的特性)**3)还发现了**AnnotationAwareAs

Java架狗师知识框架速查表 263

Spring Framework 5.0.13官方原版源码深度解析与实战

理解AOP的前提是掌握其基本术语体系。以下是对关键概念的形式化描述及其在Spring中的具体体现:连接点(Joinpoint):指程序执行过程中能够被拦截的特定位置,例如方法调用、异常抛出、字段访问等。在Spring AOP中,仅支持方法级别的连接点,即只能拦截public方法的调用。切点(Pointcut):用于匹配一个或多个连接点的表达式。它是对连接点的筛选条件,决定了哪些方法需要被增强。Spring使用AspectJ的切点表达式语法(如),并通过Pointcut。

weixin_35794316的博客 1074

NI USB-8506 LIN通讯配置全攻略:从驱动安装到NI MAX硬件检测

本文详细介绍了NI USB-8506设备的LIN通讯配置全流程,从驱动安装到NI MAX硬件检测。重点讲解了NI-CAN和NI-XNET驱动的安装避坑技巧,以及LIN通讯参数配置的实战经验,帮助用户快速掌握车载LIN总线通讯技术,提升工作效率。

weixin_29171087的博客 600

语音增强评价指标全解析:从MOS到eSTOI的实战指南

本文全面解析了语音增强领域的核心评价指标,包括主观评价的黄金标准MOS,以及PESQ、STOI、eSTOI等关键客观指标。文章详细阐述了各指标的原理、适用场景与实战计算方法,并提供了Python代码示例,旨在帮助开发者和研究者建立科学的评估体系,以平衡语音可懂度与音质,从而优化算法效果。

yhn456789的博客 1201

模拟退火算法详细讲解(含实例python代码)

模拟退火算法详细讲解(含实例python代码)(一)模拟退火算法简介三级目录(二)模拟退火算法原理(1)基本思想(2)算法步骤(3)参数控制(三)实例分析 最近老师要求做模拟退火算法实验,看了很多博客之后感觉还是不太清楚,最后问了老师之后才搞明白。想把自己的理解写下来,帮助大家更好的理解。本篇文章是在另一篇博客的基础上加了一下自己的理解,然后又把我们在实验中的实例写下来,还有参考代码。希望大家看了之后能够加深对模拟退火算法的理解。 另一篇博客链接: 深度学习 — 模拟退火算法详解. (一)模拟退火算法简介

weixin_48241292的博客 17万+

TMC步进电机驱动stealthChop

经过九九八十一难,芯片手册拜读了好多遍,找了好多资料,问了好多人使用TMC2660驱动步进电机终于成功了。最后的问题居然是硬件把管脚画错了,然后我又没有检查,哭。。。

那可真是太开心了的博客 4490

6-3 根据派生类写出基类(Java解法)(PTA)

主要是看main函数里调用了什么方法和输出样例,一一对应一下有思路再写就很简单.建议使用IDEA自动生成,可以少写很多代码。

u012137663的博客 1143

GEE 案例:基于陆地卫星8号(大气层顶反射率数据)LANDSAT/LC08/C02/T1_TOA的生态遥感指数RSEI计算、可视化和下载

摘要:基于GEE的遥感生态指数(RSEI)计算方法 本文介绍了利用Google Earth Engine(GEE)平台计算遥感生态指数(RSEI)的全流程方法。主要内容包括:研究区域定义、Landsat8数据获取与去云预处理、关键生态指标计算(地表温度LST、植被指数NDVI、湿度指数WET和建筑指数NDBSI)、主成分分析(PCA)降维处理,以及结果可视化与数据导出。通过结合多源遥感指标并采用归一化处理,构建了综合反映区域生态环境质量的RSEI指数。该方法为生态环境监测提供了高效的技术方案,所有计算过程均

此星光明博客 466

DISK配置服务器软RAID工作任务(RAID1+BITLOCKER)-winserver4

DISK配置服务器软RAID工作任务(RAID1+BITLOCKER)-winserver4

新时代先锋的博客 7299

YOLOv11 改进入门篇 | 从原理到代码:CBAM 注意力模块的插入与涨点验证

本文介绍了如何在YOLOv11模型中插入CBAM注意力模块以提升检测性能。CBAM包含通道和空间注意力两个子模块,通过双池化操作和MLP学习特征权重。文章详细解析了CBAM原理,提供了PyTorch实现代码,并建议在YOLOv11的Stage-3和Stage-4之间插入CBAM。实验表明,该方法在COCO数据集上使nano和small模型的mAP提升1.7-2.3个百分点,参数量仅增加0.27%,推理延迟增加0.1ms。最后给出了插入位置选择、通道缩减比等调优建议,验证了CBAM作为轻量级注意力模块的有效性

shrgegrb的博客 1302

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

上一篇: RocketMQ的push消费方式实现的简直不要太聪明
下一篇: Java面试题之多线程与并发
java_lujj
博客等级 码龄4年 18粉丝 152原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值