手写Spring-第十一章-用动态代理实现AOP核心功能
admin
2024-03-21 09:25:45
0

前言

前面经过了十章的漫长练习,我们终于把Spring的IOC部分完善的差不多了。那么从这一章开始,我们就要步入Spring的另外半壁江山:AOP了。AOP,也就是面向切面编程。它可以实现对业务逻辑的各个部分进行隔离,从而降低各个模块之间的耦合度,提高代码可复用性。

我们平时对Spring AOP的应用,最常见的情况可能就是对方法执行的监控了。比如在方法执行前后输出一些日志之类的。那么Spring是如何实现这个功能的呢?答案就是动态代理。我们最终使用的类,实际上已经不是我们最原始的类了,而是被Spring代理过的类。而一旦这个类被代理了,那就意味着Spring可以在执行前后做任何自己想做的事情了。就像租房子时候的黑中介一样,给你加上各种条款还有高额的中介费(泣。

工程结构

├─src
│  ├─main
│  │  ├─java
│  │  │  └─com
│  │  │      └─akitsuki
│  │  │          └─springframework
│  │  │              ├─aop
│  │  │              │  │  AdvisedSupport.java
│  │  │              │  │  ClassFilter.java
│  │  │              │  │  MethodMatcher.java
│  │  │              │  │  PointCut.java
│  │  │              │  │  TargetSource.java
│  │  │              │  │  
│  │  │              │  ├─aspect
│  │  │              │  │      AspectJExpressionPointcut.java
│  │  │              │  │    
│  │  │              │  └─framework
│  │  │              │          AopProxy.java
│  │  │              │          Cglib2AopProxy.java
│  │  │              │          JdkDynamicAopProxy.java
│  │  │              │          ReflectiveMethodInvocation.java
│  │  │              │        
│  │  │              ├─beans
│  │  │              │  ├─exception
│  │  │              │  │      BeanException.java
│  │  │              │  │    
│  │  │              │  └─factory
│  │  │              │      │  Aware.java
│  │  │              │      │  BeanClassLoaderAware.java
│  │  │              │      │  BeanFactory.java
│  │  │              │      │  BeanFactoryAware.java
│  │  │              │      │  BeanNameAware.java
│  │  │              │      │  ConfigurableListableBeanFactory.java
│  │  │              │      │  DisposableBean.java
│  │  │              │      │  FactoryBean.java
│  │  │              │      │  HierarchicalBeanFactory.java
│  │  │              │      │  InitializingBean.java
│  │  │              │      │  ListableBeanFactory.java
│  │  │              │      │  
│  │  │              │      ├─config
│  │  │              │      │      AutowireCapableBeanFactory.java
│  │  │              │      │      BeanDefinition.java
│  │  │              │      │      BeanDefinitionRegistryPostProcessor.java
│  │  │              │      │      BeanFactoryPostProcessor.java
│  │  │              │      │      BeanPostProcessor.java
│  │  │              │      │      BeanReference.java
│  │  │              │      │      ConfigurableBeanFactory.java
│  │  │              │      │      DefaultSingletonBeanRegistry.java
│  │  │              │      │      PropertyValue.java
│  │  │              │      │      PropertyValues.java
│  │  │              │      │      SingletonBeanRegistry.java
│  │  │              │      │    
│  │  │              │      ├─support
│  │  │              │      │      AbstractAutowireCapableBeanFactory.java
│  │  │              │      │      AbstractBeanDefinitionReader.java
│  │  │              │      │      AbstractBeanFactory.java
│  │  │              │      │      BeanDefinitionReader.java
│  │  │              │      │      BeanDefinitionRegistry.java
│  │  │              │      │      CglibSubclassingInstantiationStrategy.java
│  │  │              │      │      DefaultListableBeanFactory.java
│  │  │              │      │      DisposableBeanAdapter.java
│  │  │              │      │      FactoryBeanRegistrySupport.java
│  │  │              │      │      InstantiationStrategy.java
│  │  │              │      │      SimpleInstantiationStrategy.java
│  │  │              │      │    
│  │  │              │      └─xml
│  │  │              │              XmlBeanDefinitionReader.java
│  │  │              │            
│  │  │              ├─context
│  │  │              │  │  ApplicationContext.java
│  │  │              │  │  ApplicationContextAware.java
│  │  │              │  │  ApplicationEvent.java
│  │  │              │  │  ApplicationEventPublisher.java
│  │  │              │  │  ApplicationListener.java
│  │  │              │  │  ConfigurableApplicationContext.java
│  │  │              │  │  
│  │  │              │  ├─event
│  │  │              │  │      AbstractApplicationEventMulticaster.java
│  │  │              │  │      ApplicationContextEvent.java
│  │  │              │  │      ApplicationEventMulticaster.java
│  │  │              │  │      ContextClosedEvent.java
│  │  │              │  │      ContextRefreshEvent.java
│  │  │              │  │      SimpleApplicationEventMulticaster.java
│  │  │              │  │    
│  │  │              │  └─support
│  │  │              │          AbstractApplicationContext.java
│  │  │              │          AbstractRefreshableApplicationContext.java
│  │  │              │          AbstractXmlApplicationContext.java
│  │  │              │          ApplicationContextAwareProcessor.java
│  │  │              │          ClasspathXmlApplicationContext.java
│  │  │              │        
│  │  │              ├─core
│  │  │              │  └─io
│  │  │              │          ClasspathResource.java
│  │  │              │          DefaultResourceLoader.java
│  │  │              │          FileSystemResource.java
│  │  │              │          Resource.java
│  │  │              │          ResourceLoader.java
│  │  │              │          UrlResource.java
│  │  │              │        
│  │  │              └─util
│  │  │                      ClassUtils.java
│  │  │                    
│  │  └─resources
│  └─test
│      └─java
│          └─com
│              └─akitsuki
│                  └─springframework
│                      │  ApiTest.java
│                      │  
│                      ├─aop
│                      │  └─aspect
│                      │          AspectJExpressionPointcutTest.java
│                      │        
│                      └─bean
│                              IUserDao.java
│                              UserDao.java
│                              UserDaoInterceptor.java

这次的内容,主要新增了一个aop包,经历了前面的练习,这些已经是小场面了。让我们开始这一次的内容吧。

事前准备

这次我们需要引入两个新的依赖,来帮助我们解析表达式,以及实现代理

        org.aspectjaspectjweaver1.9.7aopallianceaopalliance1.0

切点表达式

回忆一下,我们在Spring中使用AOP的时候,一般都是用一个表达式来表示我们需要切入的方法或者类。那么我们首先就要来实现这一功能。

首先,我们需要一个切入点

package com.akitsuki.springframework.aop;/*** 切入点接口** @author ziling.wang@hand-china.com* @date 2022/12/5 9:51*/
public interface PointCut {/*** 获取classFilter* @return*/ClassFilter getClassFilter();/*** 获取method匹配器* @return*/MethodMatcher getMethodMatcher();
}

这里的 ClassFilterMethodMatcher,我们会在下面进行介绍。这两个类,分别是用来匹配类与方法的。

我们来看看ClassFilter

package com.akitsuki.springframework.aop;/*** 类匹配接口,用于切点找到给定的接口和目标类** @author ziling.wang@hand-china.com* @date 2022/12/5 9:54*/
public interface ClassFilter {/*** 是否匹配** @param clazz 要匹配的类* @return result*/boolean matches(Class clazz);
}

简单易懂的一个方法,就是用来判断传入的类是否匹配的

我们再来看MethodMatcher

package com.akitsuki.springframework.aop;import java.lang.reflect.Method;/*** 方法匹配接口,找到表达式范围内匹配下的目标类和方法** @author ziling.wang@hand-china.com* @date 2022/12/5 9:55*/
public interface MethodMatcher {/*** 是否匹配** @param method 要匹配的方法* @param clazz  要匹配的类* @return result*/boolean matches(Method method, Class clazz);
}

和上面类似,这次是判断方法是否匹配。

有了这些接口作为框架,我们就可以实现我们的表达式匹配类了

package com.akitsuki.springframework.aop.aspect;import com.akitsuki.springframework.aop.ClassFilter;
import com.akitsuki.springframework.aop.MethodMatcher;
import com.akitsuki.springframework.aop.PointCut;
import org.aspectj.weaver.tools.PointcutExpression;
import org.aspectj.weaver.tools.PointcutParser;
import org.aspectj.weaver.tools.PointcutPrimitive;import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;/*** @author ziling.wang@hand-china.com* @date 2022/12/5 9:58*/
public class AspectJExpressionPointcut implements PointCut, ClassFilter, MethodMatcher {private static final Set SUPPORTED_PRIMITIVES = new HashSet<>();static {SUPPORTED_PRIMITIVES.add(PointcutPrimitive.EXECUTION);}private final PointcutExpression pointcutExpression;public AspectJExpressionPointcut(String expression) {PointcutParser pointcutParser = PointcutParser.getPointcutParserSupportingSpecifiedPrimitivesAndUsingSpecifiedClassLoaderForResolution(SUPPORTED_PRIMITIVES, this.getClass().getClassLoader());pointcutExpression = pointcutParser.parsePointcutExpression(expression);}@Overridepublic boolean matches(Class clazz) {return pointcutExpression.couldMatchJoinPointsInType(clazz);}@Overridepublic boolean matches(Method method, Class clazz) {return pointcutExpression.matchesMethodExecution(method).alwaysMatches();}@Overridepublic ClassFilter getClassFilter() {return this;}@Overridepublic MethodMatcher getMethodMatcher() {return this;}
}

我们来分析一下。首先,我们维护了一个Set,用来表示我们支持的匹配方式。然后在静态代码块中,加入了execution表达式的支持。然后我们看构造方法,主要内容是通过传入的表达式字符串,初始化我们的表达式解析器。最后我们来看看对方法内容的实现。可以看到主要都是通过表达式解析器来进行判断传入的类或者方法,是否满足当前的表达式。

对切面信息进行包装

我们既然要对一个对象进行代理,首先我们需要一个类来表示它。怎么理解呢,有些像我们前面的Bean定义一样,需要一个类来存放一些被代理类的信息。

package com.akitsuki.springframework.aop;/*** 被代理的目标对象** @author ziling.wang@hand-china.com* @date 2022/12/5 11:11*/
public class TargetSource {private final Object target;public TargetSource(Object target) {this.target = target;}public Class[] getTargetClass() {return target.getClass().getInterfaces();}public Object getTarget() {return target;}
}

可以看到,我们用TargetSource类,来表示被代理的对象。类中维护了一个Object属性来存储被代理对象,同时提供了getTargetClass方法,来获取对象所实现的接口。这一点很重要,并不是获取对象本身,而是获取对象的接口。因为我们用JDK的动态代理来进行实现的话,被代理的对象必须要实现接口,所以我们也需要一个方法来提供这些接口。

接下来,我们还需要一个类,用来包装上面的这些信息。它的作用有些类似于我们平时使用的DTO,主要是对需要用到的信息起到一个包装汇总作用。

package com.akitsuki.springframework.aop;import lombok.Getter;
import lombok.Setter;
import org.aopalliance.intercept.MethodInterceptor;/*** 切面通知属性包装类,主要是方便管理** @author ziling.wang@hand-china.com* @date 2022/12/5 11:08*/
@Getter
@Setter
public class AdvisedSupport {/*** 被代理的目标对象*/private TargetSource targetSource;/*** 方法拦截器*/private MethodInterceptor methodInterceptor;/*** 方法匹配器*/private MethodMatcher methodMatcher;}

可以看到,它包含了被代理的对象、方法拦截器和方法匹配器三个内容。

开始代理!

上面铺垫了这么多,我们终于要开始今天的重头戏了。我们知道常用的代理,有JDK的动态代理,还有Cglib提供的代理。所以我们首先需要一个接口,用来抽象我们的代理方式。

package com.akitsuki.springframework.aop.framework;/*** 获取代理类,因为代理的方式可能有多种实现** @author ziling.wang@hand-china.com* @date 2022/12/5 11:29*/
public interface AopProxy {/*** 获取代理类** @return resuLt*/Object getProxy();
}

我们这次准备提供JDK和Cglib两种代理的实现,首先来看JDK的实现方式

package com.akitsuki.springframework.aop.framework;import com.akitsuki.springframework.aop.AdvisedSupport;
import org.aopalliance.intercept.MethodInterceptor;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;/*** @author ziling.wang@hand-china.com* @date 2022/12/5 11:30*/
public class JdkDynamicAopProxy implements AopProxy, InvocationHandler {private final AdvisedSupport advisedSupport;public JdkDynamicAopProxy(AdvisedSupport advisedSupport) {this.advisedSupport = advisedSupport;}@Overridepublic Object getProxy() {return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),advisedSupport.getTargetSource().getTargetClass(),this);}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if (advisedSupport.getMethodMatcher().matches(method, advisedSupport.getTargetSource().getTarget().getClass())) {MethodInterceptor methodInterceptor = advisedSupport.getMethodInterceptor();return methodInterceptor.invoke(new ReflectiveMethodInvocation(advisedSupport.getTargetSource().getTarget(), method, args));}return method.invoke(advisedSupport.getTargetSource().getTarget(), args);}
}

可以看到,首先这个类实现了 AopProxyInvocationHandler接口。实现了 InvocationHandler接口,就意味着当前这个类可以作为JDK动态代理类来使用了。getProxy方法中,通过 Proxy.newProxyInstance方法,来动态生成代理对象。接下来我们看 invoke方法。这个方法是代理类在调用方法时,会被拦截执行的一个方法。可以看到这里实现的内容是,通过方法匹配器来判断当前方法是否需要被拦截,如果需要被拦截,则通过方法拦截器来进行调用。否则,直接通过反射进行调用方法。

这里还可以看到有一个 ReflectiveMethodInvocation,它其实也是起到一个包装作用,实现了 MethodInvocation接口。主要内容的实现 proceed方法,实际上还是对方法进行反射调用。

package com.akitsuki.springframework.aop.framework;import org.aopalliance.intercept.MethodInvocation;import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Method;/*** @author ziling.wang@hand-china.com* @date 2022/12/5 15:39*/
public class ReflectiveMethodInvocation implements MethodInvocation {protected final Object target;protected final Method method;protected final Object[] arguments;public ReflectiveMethodInvocation(Object target, Method method, Object[] arguments) {this.target = target;this.method = method;this.arguments = arguments;}@Overridepublic Method getMethod() {return method;}@Overridepublic Object[] getArguments() {return arguments;}@Overridepublic Object proceed() throws Throwable {return method.invoke(target, arguments);}@Overridepublic Object getThis() {return target;}@Overridepublic AccessibleObject getStaticPart() {return method;}
}

接下来我们看Cglib的实现方式

package com.akitsuki.springframework.aop.framework;import com.akitsuki.springframework.aop.AdvisedSupport;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;import java.lang.reflect.Method;/*** @author ziling.wang@hand-china.com* @date 2022/12/5 15:45*/
public class Cglib2AopProxy implements AopProxy {private final AdvisedSupport advisedSupport;public Cglib2AopProxy(AdvisedSupport advisedSupport) {this.advisedSupport = advisedSupport;}@Overridepublic Object getProxy() {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(advisedSupport.getTargetSource().getTarget().getClass());enhancer.setInterfaces(advisedSupport.getTargetSource().getTargetClass());enhancer.setCallback(new DynamicAdvisedInterceptor(advisedSupport));return enhancer.create();}private static class DynamicAdvisedInterceptor implements MethodInterceptor {private final AdvisedSupport advisedSupport;public DynamicAdvisedInterceptor(AdvisedSupport advisedSupport) {this.advisedSupport = advisedSupport;}@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {CglibMethodInvocation methodInvocation =new CglibMethodInvocation(advisedSupport.getTargetSource().getTarget(), method, objects, methodProxy);if (advisedSupport.getMethodMatcher().matches(method, advisedSupport.getTargetSource().getTarget().getClass())) {return advisedSupport.getMethodInterceptor().invoke(methodInvocation);}return methodInvocation.proceed();}}private static class CglibMethodInvocation extends ReflectiveMethodInvocation {private final MethodProxy methodProxy;public CglibMethodInvocation(Object target, Method method, Object[] arguments, MethodProxy methodProxy) {super(target, method, arguments);this.methodProxy = methodProxy;}@Overridepublic Object proceed() throws Throwable {return this.methodProxy.invoke(this.target, this.arguments);}}
}

看起来很复杂,还有两个静态的内部类。但其实仔细分析一下的话,会发现也不是很难。我们先来看 getProxy的内容,可以看到,这里是通过Enhancer创建了代理对象,重点在于Callback的内容。这里的Callback,实际上和上面JDK动态代理中的invoke功能是一样的,都是代理后拦截执行的逻辑。这里需要传入一个Callback类型的对象,Callback实际上是一个标记接口,我们之前也接触过很多次标记接口了,有很多类都实现了Callback接口,我们一般常用的是 MethodInterceptor,也就是方法拦截器。往下看,我们的第一个静态内部类,实际上就是一个方法拦截器,然后我们看具体内容,实际上还是通过方法匹配器来进行判断,如果匹配就调用方法拦截器来执行,和JDK动态代理是一样的玩法,如果不匹配,则调用 methodInvocation来执行。这里的 MethodInvocation,就是我们下面的静态内部类,可以看到它继承了我们上面提到的 ReflectiveMethodInvocation,所以其实本质上它们没什么区别。主要就是这里的执行,是通过方法代理 MethodProxy来进行的,而 ReflectiveMethodInvocation则是通过反射直接执行的,就这么一点区别。

测试!

好了,终于又到了测试环节。总觉得这次虽然新加了不少类,但是实际上内容却不是很多。这次的测试,我们和前面的Spring IOC内容决裂了!已经没有什么Context了!也没有什么Bean定义了!回归最初的美好吧!

不过我们还是得有个bean(笑)

由于JDK动态代理要求类必须实现接口,所以我们先得给整一个接口

package com.akitsuki.springframework.bean;/*** @author ziling.wang@hand-china.com* @date 2022/12/5 17:26*/
public interface IUserDao {String queryUserName(Long id);
}

嗯,简单易懂,接下来是我们永远跑不了的bean

package com.akitsuki.springframework.bean;import java.util.HashMap;
import java.util.Map;/*** @author ziling.wang@hand-china.com* @date 2022/11/8 14:42*/
public class UserDao implements IUserDao {private static final Map userMap = new HashMap<>();static {userMap.put(1L, "akitsuki");userMap.put(2L, "toyosaki");userMap.put(3L, "kugimiya");userMap.put(4L, "hanazawa");userMap.put(5L, "momonogi");}@Overridepublic String queryUserName(Long id) {return userMap.get(id);}
}

没啥大变化,就是实现了上面的接口

下面是重点了,我们要实现自定义的方法拦截器,也就是我们切面的具体内容,需要实现 MethodInerceptor接口。

package com.akitsuki.springframework.bean;import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;/*** @author ziling.wang@hand-china.com* @date 2022/12/5 16:51*/
public class UserDaoInterceptor implements MethodInterceptor {@Overridepublic Object invoke(MethodInvocation methodInvocation) throws Throwable {long start = System.currentTimeMillis();try {return methodInvocation.proceed();} finally {System.out.println("+++++ AOP 方法执行监控 +++++");System.out.println("方法名称:" + methodInvocation.getMethod().getName());System.out.println("方法耗时:" + (System.currentTimeMillis() - start) + "ms");System.out.println("----- AOP 方法监控结束 -----");}}
}

打印一下方法执行的信息,很简单。

下面,是真正的测试类

package com.akitsuki.springframework;import com.akitsuki.springframework.aop.AdvisedSupport;
import com.akitsuki.springframework.aop.TargetSource;
import com.akitsuki.springframework.aop.aspect.AspectJExpressionPointcut;
import com.akitsuki.springframework.aop.framework.Cglib2AopProxy;
import com.akitsuki.springframework.aop.framework.JdkDynamicAopProxy;
import com.akitsuki.springframework.bean.IUserDao;
import com.akitsuki.springframework.bean.UserDao;
import com.akitsuki.springframework.bean.UserDaoInterceptor;
import org.junit.Test;/*** @author ziling.wang@hand-china.com* @date 2022/12/5 16:46*/
public class ApiTest {@Testpublic void test() {IUserDao userDao = new UserDao();AdvisedSupport advisedSupport = new AdvisedSupport();advisedSupport.setTargetSource(new TargetSource(userDao));advisedSupport.setMethodInterceptor(new UserDaoInterceptor());advisedSupport.setMethodMatcher(new AspectJExpressionPointcut("execution(* com.akitsuki.springframework.bean.IUserDao.*(..))"));IUserDao proxyJdk = (IUserDao) new JdkDynamicAopProxy(advisedSupport).getProxy();assert proxyJdk != null;System.out.println("测试结果:" + proxyJdk.queryUserName(1L));IUserDao proxyCglib = (IUserDao) new Cglib2AopProxy(advisedSupport).getProxy();System.out.println("测试结果:" + proxyCglib.queryUserName(2L));}
}

测试结果

+++++ AOP 方法执行监控 +++++
方法名称:queryUserName
方法耗时:0ms
----- AOP 方法监控结束 -----
测试结果:akitsuki
+++++ AOP 方法执行监控 +++++
方法名称:queryUserName
方法耗时:12ms
----- AOP 方法监控结束 -----
测试结果:toyosakiProcess finished with exit code 0

可以看到,我们切面中的内容已经正常打印出来了。这次我们的表达式拦截的是IUserDao接口的所有方法,分别创建了JDK动态代理和Cglib动态代理两种方式来进行测试,都能够正常进行工作。

不过我们也可以看到,这种方式终究还是有些繁琐,而且没有和Spring很好的结合起来,显得有些割裂。那么就留到下一章再进行完善吧。

相关源码可以参考我的gitee:https://gitee.com/akitsuki-kouzou/mini-spring,这里对应的代码是mini-spring-11

相关内容

热门资讯

linux入门---制作进度条 了解缓冲区 我们首先来看看下面的操作: 我们首先创建了一个文件并在这个文件里面添加了...
C++ 机房预约系统(六):学... 8、 学生模块 8.1 学生子菜单、登录和注销 实现步骤: 在Student.cpp的...
A.机器学习入门算法(三):基... 机器学习算法(三):K近邻(k-nearest neigh...
数字温湿度传感器DHT11模块... 模块实例https://blog.csdn.net/qq_38393591/article/deta...
有限元三角形单元的等效节点力 文章目录前言一、重新复习一下有限元三角形单元的理论1、三角形单元的形函数(Nÿ...
Redis 所有支持的数据结构... Redis 是一种开源的基于键值对存储的 NoSQL 数据库,支持多种数据结构。以下是...
win下pytorch安装—c... 安装目录一、cuda安装1.1、cuda版本选择1.2、下载安装二、cudnn安装三、pytorch...
MySQL基础-多表查询 文章目录MySQL基础-多表查询一、案例及引入1、基础概念2、笛卡尔积的理解二、多表查询的分类1、等...
keil调试专题篇 调试的前提是需要连接调试器比如STLINK。 然后点击菜单或者快捷图标均可进入调试模式。 如果前面...
MATLAB | 全网最详细网... 一篇超超超长,超超超全面网络图绘制教程,本篇基本能讲清楚所有绘制要点&#...
IHome主页 - 让你的浏览... 随着互联网的发展,人们越来越离不开浏览器了。每天上班、学习、娱乐,浏览器...
TCP 协议 一、TCP 协议概念 TCP即传输控制协议(Transmission Control ...
营业执照的经营范围有哪些 营业执照的经营范围有哪些 经营范围是指企业可以从事的生产经营与服务项目,是进行公司注册...
C++ 可变体(variant... 一、可变体(variant) 基础用法 Union的问题: 无法知道当前使用的类型是什...
血压计语音芯片,电子医疗设备声... 语音电子血压计是带有语音提示功能的电子血压计,测量前至测量结果全程语音播报࿰...
MySQL OCP888题解0... 文章目录1、原题1.1、英文原题1.2、答案2、题目解析2.1、题干解析2.2、选项解析3、知识点3...
【2023-Pytorch-检... (肆十二想说的一些话)Yolo这个系列我们已经更新了大概一年的时间,现在基本的流程也走走通了,包含数...
实战项目:保险行业用户分类 这里写目录标题1、项目介绍1.1 行业背景1.2 数据介绍2、代码实现导入数据探索数据处理列标签名异...
记录--我在前端干工地(thr... 这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 前段时间接触了Th...
43 openEuler搭建A... 文章目录43 openEuler搭建Apache服务器-配置文件说明和管理模块43.1 配置文件说明...