当前位置: 首页 > article >正文

Spring源码(12)-- Aop源码

Aop

切面编程包括切面(Aspect),连接点(Joinpoint)、通知(Advice)、切点(Pointcut)、引入(Introduction)

通知(Advice)又分为前置通知,后置通知,返回通知,环绕通知,异常通知等。

AOP 基础知识:

详情见: https://blog.csdn.net/sinat_32502451/article/details/142291052

Aop 注解:

@Aspect表明整个类是一个切面。

@Pointcut注解声明一个切点。

@Before:前置通知,在方法执行前执行。

@After:后置通知, 在方法执行后执行。

@AfterThrowing:异常通知, 在方法抛出异常后执行。

@AfterReturning :返回通知,在方法成功返回后执行。

@Around:环绕通知。可以在方法的前后执行逻辑。

Aop 示例:

可以在 com.example.demo.controller. 这个文件夹下创建一个 Controller 类,通过 http请求调用接口,触发AOP逻辑。
通过以下的这个示例,学习 @Aspect 、@Pointcut 、@Before、 @After、 @AfterReturning 、@Around 的运用及源码。

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;


@Aspect
@Component
public class AopExample {


    /**
     * 定义切点 切点为 com.example.demo..controller 下所有的类
     *
     * 第一个 * 表示任意类,
     * 第二个 * 表示任意方法。
     * .. 表示匹配任意数量任意类型的参数。
     */
    @Pointcut("execution(* com.example.demo..controller.*.*(..))")
    public void pointcut() {
        log.info("AopExample pointcut ..");
    }

    /**
     * 前置通知
     */
    @Before(value = "pointcut()")
    public void aopBefore(JoinPoint joinPoint )  {
        log.info("AopExample aopBefore ..");
    }

    /**
     * 后置通知
     */
    @After(value = "pointcut()")
    public void aopAfter(JoinPoint joinPoint )  {
        log.info("AopExample aopAfter ..");
    }

    /**
     * 返回通知
     */
    @AfterReturning(value = "pointcut()")
    public void aopAfterReturning(JoinPoint joinPoint) throws Throwable {
        log.info("AopExample aopAfterReturning ..");
    }


    /**
     * 异常通知
     */
    @AfterThrowing(value = "pointcut()")
    public void aopAfterThrowing(JoinPoint joinPoint) throws Throwable {
        log.info("AopExample aopAfterThrowing ..");
    }


    /**
     * 环绕通知
     */
    @Around(value = "pointcut()")
    public Object aopAround(ProceedingJoinPoint joinPoint) throws Throwable {
        String className = joinPoint.getTarget().getClass().getName();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //获取方法路径及方法名称
        String methodName = className + "#" + signature.getMethod().getName();
        log.info("AopExample class aopAround start.methodName:{}", methodName);
        
        //在这里,会执行pointcut对应的方法
        Object proceed = joinPoint.proceed();
        log.info("AopExample class aopAround end.");

        return proceed;
    }

}

1.Aspect (切面)

切面(Aspect) 。

代码: org.aspectj.lang.annotation.Aspect

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Aspect {

    /**
     * 表达式
     */
    public String value() default "";
}
  • AbstractAspectJAdvisorFactory:

代码:org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory#isAspect

判断提供的方法上是否有 @Aspect 注解。如果给定方法本身不直接存在注解,则遍历其超方法(即从超类和接口)。
在Spring启动时就会进行判断。

    /**
    *  判断是否为 Aspect(切面)。
    */
    @Override
    public boolean isAspect(Class<?> clazz) {
        return (hasAspectAnnotation(clazz) && !compiledByAjc(clazz));
    }

    /**
    *  判断是否有 @Aspect 注解。
    */
    private boolean hasAspectAnnotation(Class<?> clazz) {
        return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null);
    }

2.JoinPoint(连接点)

JoinPoint(连接点)可以获取类的信息,包括类名、方法名、参数等(joinPoint.getArgs()), 还可以执行目标对象方法的逻辑(joinPoint.proceed())。

代码: org.aspectj.lang.JoinPoint

/**
 * 提供对连接点可用状态及其静态信息的反射访问。
 * 此信息可使用thisJoinPoint从advice中获得。
 * 这种反射信息的主要用途是用于跟踪和记录应用程序。
 */
public interface JoinPoint {

    String toString();

    /**
     * 返回连接点的缩写字符串表示形式。
     */
    String toShortString();

    /**
     * 返回连接点的扩展字符串表示形式。
     */
    String toLongString();

    /**
     * 返回当前正在执行的对象。
     */
    Object getThis();

    /**
     * 返回目标对象。
     */
    Object getTarget();

    /**
     * 返回此连接点处的参数。
     */
    Object[] getArgs();

    /**  
     *
     * 返回连接点处的签名。
     */
    Signature getSignature();

    /** 
     *  返回与连接点对应的源位置。
     */
    SourceLocation getSourceLocation();

    /** 
     *  返回一个表示连接点类型的字符串。
     */
    String getKind();

   /**    
     *  此辅助对象仅包含有关连接点的静态信息。
     */
    public interface StaticPart {
        
        Signature getSignature();

        SourceLocation getSourceLocation();

        String getKind();
        
        /**
         *  返回此 JoinPoint.StaticPart 的id。
         */
        int getId();

        String toString();

        String toShortString();

        String toLongString();
    }

    public interface EnclosingStaticPart extends StaticPart {}

    /**
     * 返回一个封装此连接点静态部分的对象。
     */
    StaticPart getStaticPart();


    /**
     * getKind()的合法返回值
     */
    static String METHOD_EXECUTION = "method-execution";
    static String METHOD_CALL = "method-call";
    static String CONSTRUCTOR_EXECUTION = "constructor-execution";
    static String CONSTRUCTOR_CALL = "constructor-call";
    static String FIELD_GET = "field-get";
    static String FIELD_SET = "field-set";
    static String STATICINITIALIZATION = "staticinitialization";
    static String PREINITIALIZATION = "preinitialization";
    static String INITIALIZATION = "initialization";
    static String EXCEPTION_HANDLER = "exception-handler";
    static String SYNCHRONIZATION_LOCK = "lock";
    static String SYNCHRONIZATION_UNLOCK = "unlock";

    static String ADVICE_EXECUTION = "adviceexecution"; 

}

Invocation

Invocation 继承了 Joinpoint 接口。
Invocation 接口表示程序中一个的调用。调用是一个连接点,可以被拦截器拦截。

public interface Invocation extends Joinpoint {

    /**
     * 以数组对象的格式,返回方法参数。可以更改此数组中的元素值以更改参数。
     */
    Object[] getArguments();

}

MethodInvocation

MethodInvocation是继承了Invocation的, 而 Invocation 又继承Joinpoint(连接点)。

MethodInvocation 是对方法调用的描述,在方法调用时提供给拦截器。方法调用是一个连接点,可以被方法拦截器拦截。

MethodInvocation 在Aop 的各种通知中经常被作为方法参数使用。通过 MethodInvocation 来执行目标对象的方法。

public interface MethodInvocation extends Invocation {

    /**
     * 获取被调用的方法。
     */
    Method getMethod();

}

3.Pointcut (切点)

  • @Pointcut 注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Pointcut {

    /**
     * 切入点表达式,允许“”这个空值作为抽象切入点的默认值
     */
    String value() default "";
    
    /**
     * 在没有调试信息的情况下编译时,或者在运行时解析切入点时,切入点中使用的任何参数的名称都不可用。
     */
    String argNames() default "";
}
  • Pointcut 接口:

代码: org.springframework.aop.Pointcut

Pointcut(切入点)由 ClassFilter 和 MethodMatcher 组成。

ClassFilter是类级别的匹配,MethodMatcher是方法级别的匹配。

/**
 * Spring切入点抽象。
 * 切入点由 ClassFilter 和 MethodMatcher 组成。
 * 这些基本术语和Pointcut本身都可以组合在一起,形成组合。
 *
 */
public interface Pointcut {

    /**
     * 返回 ClassFilter。
     * 
     */
    ClassFilter getClassFilter();

    /**
     * 返回 MethodMatcher.
     * 
     */
    MethodMatcher getMethodMatcher();


    /**
     * 匹配的规范Pointcut实例。
     */
    Pointcut TRUE = TruePointcut.INSTANCE;

}

4.Advice

Advice 是通知的接口。

Advice 的实现可以是任何类型的通知,例如拦截器。

代码: org.aopalliance.aop.Advice

public interface Advice {

}

Interceptor

Interceptor 是拦截器的接口。Interceptor 实现了 Advice。

public interface Interceptor extends Advice {

}

MethodInterceptor

MethodInterceptor 是一个拦截器接口,可以在目标方法调用的前后执行额外的处理,应用非常广泛。

Aop 的各种通知 AspectJMethodBeforeAdvice、 AspectJAfterAdvice、AspectJAroundAdvice 等,
以及 TransactionInterceptor 都是通过 MethodInterceptor实现的。

此处的invoke()方法参数是 MethodInvocation, MethodInvocation是继承了Invocation的, 而 Invocation 又继承Joinpoint(连接点)。

@FunctionalInterface
public interface MethodInterceptor extends Interceptor {
    
    /**
     * 实现此方法以在调用前后执行额外的处理。此处的方法参数是 MethodInvocation, MethodInvocation是间接继承了Joinpoint(连接点)的。 
     */
    Object invoke(MethodInvocation invocation) throws Throwable;

}

AbstractAspectJAdvice

Advice 抽象类。

AOP的基类,包装 AspectJ切面的Advice方法。

代码: org.springframework.aop.aspectj.AbstractAspectJAdvice#invokeAdviceMethod

invokeAdviceMethod() 这个方法,调用的频率非常高。

  
    // 通过JoinPoint(连接点),调用各种 Advice 方法。
    protected Object invokeAdviceMethod(JoinPoint jp, @Nullable JoinPointMatch jpMatch,
            @Nullable Object returnValue, @Nullable Throwable t) throws Throwable {

        return invokeAdviceMethodWithGivenArgs(argBinding(jp, jpMatch, returnValue, t));
    }

    protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable {
        Object[] actualArgs = args;
        if (this.aspectJAdviceMethod.getParameterCount() == 0) {
            actualArgs = null;
        }
        try {
            ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);
            // 调用 advice 方法。
            return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);
        }
        catch (IllegalArgumentException ex) {
            throw new AopInvocationException("Mismatch on arguments to advice method [" +
                    this.aspectJAdviceMethod + "]; pointcut expression [" +
                    this.pointcut.getPointcutExpression() + "]", ex);
        }
        catch (InvocationTargetException ex) {
            throw ex.getTargetException();
        }
    }

AspectJMethodBeforeAdvice

前置通知,添加了 @Before 注解就会调用,在方法执行后执行。

AspectJMethodBeforeAdvice 实现了 MethodBeforeAdvice 接口。

代码: org.springframework.aop.aspectj.AspectJMethodBeforeAdvice

可以看到,这里面就调用了 AbstractAspectJAdvice 抽象类里的 invokeAdviceMethod() 方法。

/**
 * 在调用方法之前调用的通知。 
 *
 */
public class AspectJMethodBeforeAdvice extends AbstractAspectJAdvice implements MethodBeforeAdvice, Serializable {

    public AspectJMethodBeforeAdvice(
            Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) {

        super(aspectJBeforeAdviceMethod, pointcut, aif);
    }


    @Override
    public void before(Method method, Object[] args, @Nullable Object target) throws Throwable {
        invokeAdviceMethod(getJoinPointMatch(), null, null);
    }

    @Override
    public boolean isBeforeAdvice() {
        return true;
    }

    @Override
    public boolean isAfterAdvice() {
        return false;
    }

}

AspectJAfterAdvice

后置通知,添加了 @After 注解就会调用,在方法执行后执行。

代码: org.springframework.aop.aspectj.AspectJAfterAdvice

可以看到,这里面也调用了 AbstractAspectJAdvice 抽象类里的 invokeAdviceMethod() 方法。


/**
 * Spring AOP Advice。在Advice方法后包装AspectJ
 *
 */
@SuppressWarnings("serial")
public class AspectJAfterAdvice extends AbstractAspectJAdvice
        implements MethodInterceptor, AfterAdvice, Serializable {

    public AspectJAfterAdvice(
            Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) {

        super(aspectJBeforeAdviceMethod, pointcut, aif);
    }


    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        try {
            //调用目标对象的目标方法。
            return mi.proceed();
        }
        finally {
            //调用AbstractAspectJAdvice 抽象类里的 invokeAdviceMethod() 方法。
            invokeAdviceMethod(getJoinPointMatch(), null, null);
        }
    }

    @Override
    public boolean isBeforeAdvice() {
        return false;
    }

    @Override
    public boolean isAfterAdvice() {
        return true;
    }

}

AfterReturningAdvice

@AfterReturning 是返回通知,在方法成功返回后执行。

代码: org.springframework.aop.AfterReturningAdvice

仅在正常方法返回时调用此 Advice,而在抛出异常时则不调用。这样的 Advice 可以看到返回值,但无法更改它。


/**
  *  仅在正常方法返回时调用此 Advice,而在抛出异常时则不调用。这样的 Advice 可以看到返回值,但无法更改它。
  *
  */
public interface AfterReturningAdvice extends AfterAdvice {

    /**
     * 给定方法成功返回后的回调。
     */
    void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable;

}

AspectJAroundAdvice

@Around 是环绕通知,在方法的前后都会调用。

添加了 @Around 的方法,会调用 AspectJAroundAdvice的 invoke() 方法。

代码: org.springframework.aop.aspectj.AspectJAroundAdvice


/**
 * Spring AOP 环绕通知(MethodInterceptor),它封装了AspectJ Advice方法。
 *
 * 
 */
@SuppressWarnings("serial")
public class AspectJAroundAdvice extends AbstractAspectJAdvice implements MethodInterceptor, Serializable {

    public AspectJAroundAdvice(
            Method aspectJAroundAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) {

        super(aspectJAroundAdviceMethod, pointcut, aif);
    }


    @Override
    public boolean isBeforeAdvice() {
        return false;
    }

    @Override
    public boolean isAfterAdvice() {
        return false;
    }

    @Override
    protected boolean supportsProceedingJoinPoint() {
        return true;
    }

    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        if (!(mi instanceof ProxyMethodInvocation)) {
            throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
        }
        ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;
        ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi);
        JoinPointMatch jpm = getJoinPointMatch(pmi);
        //调用通知的方法。
        return invokeAdviceMethod(pjp, jpm, null, null);
    }

    /**
     * 返回当前调用的ProceedingJoinPoint,如果它还没有绑定到线程,则延迟实例化它。
     */
    protected ProceedingJoinPoint lazyGetProceedingJoinPoint(ProxyMethodInvocation rmi) {
        return new MethodInvocationProceedingJoinPoint(rmi);
    }

}

5.Advisor

  • Advisor = Pointcut + Advice

Advisor 相当于 Pointcut 加上 Advice 。
此接口不供Spring用户使用,但允许支持不同类型的Advice。

代码: org.springframework.aop.Advisor

/**
  *  基础接口,用于维持AOP通知(在连接点采取的行动)和确定通知适用性的过滤器(如切入点pointCut)。
  *  
  */
public interface Advisor {

    /**
     * 如果尚未配置正确的通知,则从getAdvice()返回空通知的常用占位符。
     */
    Advice EMPTY_ADVICE = new Advice() {};


    /**
     * 返回此切面的通知部分。通知可以是拦截通知、事前通知、抛出通知等
     */
    Advice getAdvice();

    /**
     * 返回此通知是与特定实例相关联(例如,创建混入)还是与从同一Spring bean工厂获得的通知类的所有实例共享。
     */
    boolean isPerInstance();

}

  • AspectJAdvisorFactory:

Advisor的工厂。

代码: org.springframework.aop.aspectj.annotation.AspectJAdvisorFactory

/**
 * 用于工厂的接口,可以从用AspectJ注解的类中创建Spring AOP Advisors。
 *
 */
public interface AspectJAdvisorFactory {

    /**
     * 确定给定的类是否是一个切面。
     */
    boolean isAspect(Class<?> clazz);

    /**
     * 校验给定的类是否为有效的AspectJ切面类。
     */
    void validate(Class<?> aspectClass) throws AopConfigException;

    /**
     * 为指定切面实例上所有带注解的 AspectJ方法构建Spring AOP Advisors。
     */
    List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory);

    /**
     * 为给定的AspectJ通知方法构建一个Spring AOP Advisor。
     */
    @Nullable
    Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory,
            int declarationOrder, String aspectName);

    /**
     * 为给定的AspectJ通知方法构建Spring AOP通知。
     */
    @Nullable
    Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut,
            MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName);

}

BeanFactoryAspectJAdvisorsBuilder

在Spring 启动时,会查找AspectJ注解的切面bean, 并构建 Advisors。

代码: org.springframework.aop.aspectj.annotation.BeanFactoryAspectJAdvisorsBuilder#buildAspectJAdvisors

AspectJAdvisorFactory 接口代码: org.springframework.aop.aspectj.annotation.AspectJAdvisorFactory


    /**
     * 在当前bean工厂中查找AspectJ注解的切面bean,并返回代表它们的Spring AOP Advisors列表。
     *  为每个AspectJ通知方法创建一个Spring Advisor。
     */
    public List<Advisor> buildAspectJAdvisors() {
        // aspectBeanNames 是指带有@Aspect注解的类名
        List<String> aspectNames = this.aspectBeanNames;

        if (aspectNames == null) {
            synchronized (this) {
                aspectNames = this.aspectBeanNames;
                //双重检查
                if (aspectNames == null) {
                    List<Advisor> advisors = new ArrayList<>();
                    aspectNames = new ArrayList<>();
                    String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
                            this.beanFactory, Object.class, true, false);
                    for (String beanName : beanNames) {
                        if (!isEligibleBean(beanName)) {
                            continue;
                        }
                        //根据类的路径,获取 Class 类型。
                        Class<?> beanType = this.beanFactory.getType(beanName);
                        if (beanType == null) {
                            continue;
                        }
                        if (this.advisorFactory.isAspect(beanType)) {
                            aspectNames.add(beanName);
                            AspectMetadata amd = new AspectMetadata(beanType, beanName);
                            if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
                                MetadataAwareAspectInstanceFactory factory =
                                        new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
                                //在这里,会获取 Advisors
                                List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
                                if (this.beanFactory.isSingleton(beanName)) {
                                    this.advisorsCache.put(beanName, classAdvisors);
                                }
                                else {
                                    this.aspectFactoryCache.put(beanName, factory);
                                }
                                advisors.addAll(classAdvisors);
                            }
                            else {
                                if (this.beanFactory.isSingleton(beanName)) {
                                    throw new IllegalArgumentException("Bean with name '" + beanName +
                                            "' is a singleton, but aspect instantiation model is not singleton");
                                }
                                MetadataAwareAspectInstanceFactory factory =
                                        new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
                                this.aspectFactoryCache.put(beanName, factory);
                                advisors.addAll(this.advisorFactory.getAdvisors(factory));
                            }
                        }
                    }
                    this.aspectBeanNames = aspectNames;
                    return advisors;
                }
            }
        }

        if (aspectNames.isEmpty()) {
            return Collections.emptyList();
        }
        List<Advisor> advisors = new ArrayList<>();
        for (String aspectName : aspectNames) {
            List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName);
            if (cachedAdvisors != null) {
                advisors.addAll(cachedAdvisors);
            }
            else {
                MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);
                advisors.addAll(this.advisorFactory.getAdvisors(factory));
            }
        }
        return advisors;
    }
    
  • getAdvisors:

在Spring 启动时,会获取 Advisors 。

代码: org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory#getAdvisors

    @Override
    public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) {
        Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
        //切面名称
        String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName();
        //校验给定的类是否为有效的AspectJ切面。
        validate(aspectClass);

        MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory =
                new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory);

        List<Advisor> advisors = new ArrayList<>();
        for (Method method : getAdvisorMethods(aspectClass)) {
            Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName);
            if (advisor != null) {
                advisors.add(advisor);
            }
        }

        // If it's a per target aspect, emit the dummy instantiating aspect.
        if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
            Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory);
            advisors.add(0, instantiationAdvisor);
        }

        // Find introduction fields.
        for (Field field : aspectClass.getDeclaredFields()) {
            Advisor advisor = getDeclareParentsAdvisor(field);
            if (advisor != null) {
                advisors.add(advisor);
            }
        }

        return advisors;
    }

    private List<Method> getAdvisorMethods(Class<?> aspectClass) {
        final List<Method> methods = new ArrayList<>();
        ReflectionUtils.doWithMethods(aspectClass, method -> {
            // Exclude pointcuts
            if (AnnotationUtils.getAnnotation(method, Pointcut.class) == null) {
                methods.add(method);
            }
        });
        methods.sort(METHOD_COMPARATOR);
        return methods;
    }
    

http://www.kler.cn/a/306046.html

相关文章:

  • react动态路由
  • [CKS] K8S ServiceAccount Set Up
  • 2024开发者浏览器必备扩展,不允许还有人不知道~
  • Django博客网站上线前准备事项
  • 后端:Aop 面向切面编程
  • 卸载一直显示在运行的应用
  • 【Linux 从基础到进阶】自动化部署工具(Jenkins、GitLab CI/CD)
  • jdk知识
  • Excel数据清洗工具:提高数据处理效率的利器
  • verilog运算符优先级
  • TCP/IP网络编程概念及Java实现TCP/IP通讯Demo
  • 论文速递!Auto-CNN-LSTM!新的锂离子电池(LIB)剩余寿命预测方法
  • WEB打点
  • Metacritic 网站中的游戏开发者和类型信息爬取
  • OpenCV-轮廓检测
  • 《深度学习》PyTorch 手写数字识别 案例解析及实现 <下>
  • 编写并运行第一个spark java程序
  • 【JavaEE】初识⽹络原理
  • 计算机毕业设计 二手闲置交易系统的设计与实现 Java实战项目 附源码+文档+视频讲解
  • python-古籍翻译
  • Leetcode面试经典150题-148.排序链表
  • 16. 池化层的基本使用 -- nn.MaxPool2d
  • 【AcWing】【Go】789. 数的范围
  • Leetcode面试经典150题-82.删除排序链表中的重复元素II
  • NISP 一级 | 5.3 电子邮件安全
  • LottieCompositionFactory.fromUrl 加载lottie的json文件