mini-spring|把AOP动态代理,融入到Bean的生命周期

本文完成 AOP 核心功能与 Spring 框架的整合,最终能通过在 Spring 配置的方式完成切面的操作。

解决问题:

怎么借着 BeanPostProcessor 把动态代理融入到 Bean 的生命周期中,以及如何组装各项切点、拦截、前置的功能和适配对应的代理器。

设计结构:

在这里插入图片描述
1.为了可以让对象创建过程中,能把xml中配置的代理对象也就是切面的一些类对象实例化,就需要用到 BeanPostProcessor 提供的方法,因为这个类的中的方法可以分别作用与 Bean 对象执行初始化前后修改 Bean 的对象的扩展信息。但这里需要集合于 BeanPostProcessor 实现新的接口和实现类,这样才能定向获取对应的类信息。
2.但因为创建的是代理对象不是之前流程里的普通对象,所以我们需要前置于其他对象的创建,所以在实际开发的过程中,需要在 AbstractAutowireCapableBeanFactory#createBean 优先完成 Bean 对象的判断,是否需要代理,有则直接返回代理对象。在Spring的源码中会有 createBean 和 doCreateBean 的方法拆分
3.这里还包括要解决方法拦截器的具体功能,提供一些 BeforeAdvice、AfterAdvice 的实现,让用户可以更简化的使用切面功能。除此之外还包括需要包装切面表达式以及拦截方法的整合,以及提供不同类型的代理方式的代理工厂,来包装我们的切面服务。

工程结构

src
├── main
│ └── java
│ └── cn.bugstack.springframework
│ ├── aop
│ │ ├── aspectj
│ │ │ ├──AspectJExpressionPointcut.java

切点表达式实现了 Pointcut、ClassFilter、MethodMatcher,三个接口定义方法,同时这个类主要是对 aspectj 包提供的表达式校验方法使用

│ │ │ ├──AspectJExpressionPointcutAdvisor.java

AspectJExpressionPointcutAdvisor 实现了 PointcutAdvisor 接口,把切面 pointcut、拦截方法 advice 和具体的拦截表达式包装在一起。

│ │ ├── framework
│ │ │ ├──adapter
│ │ │ │ └──MethodBeforeAdviceInterceptor.java
│ │ │ ├── autoproxy
│ │ │ │ └── DefaultAdvisorAutoProxyCreator
│ │ │ ├── AopProxy.java
│ │ │ ├── Cglib2AopProxy.java
│ │ │ ├──JdkDynamicAopProxy.java
│ │ │ ├──ProxyFactory.java
│ │ │ ├── ReflectiveMethodInvocation.java
│ │ ├── AdvisedSupport.java

AdvisedSupport,主要是用于把代理、拦截、匹配的各项属性包装到一个类中,方便在 Proxy 实现类进行使用。这和你的业务开发中包装入参是一个道理

│ │ ├── Advisor.java

获得切面 由PointcutAdvisor实现接口

│ │ ├── BeforeAdvice.java(接口)
│ │ ├── ClassFilter.java

定义类匹配类,用于切点找到给定的接口和目标类。

│ │ ├── MethodBeforeAdvice.java

实现BeforeAdvice接口

│ │ ├── MethodMatcher.java

方法匹配,找到表达式范围内匹配下的目标类和方法。在上文的案例中有所体现:methodMatcher.matches(method, targetObj.getClass())

│ │ ├── Pointcut.java

切入点接口,定义用于获取 ClassFilter、MethodMatcher 的两个类

│ │ ├── PointcutAdvisor.java

承担了 Pointcut 和 Advice 的组合,Pointcut 用于获取 JoinPoint,而 Advice 决定于 JoinPoint 执行什么操作。

│ │ ├── TargetSource.java
│ ├── beans
│ │ ├── factory
│ │ │ ├── factory
│ │ │ │ ├── AutowireCapableBeanFactory.java
│ │ │ │ ├── BeanDefinition.java(实体类)

初始化和销毁

│ │ │ │ ├── BeanFactoryPostProcessor.java
│ │ │ │ ├── BeanPostProcessor.java
│ │ │ │ ├── BeanReference.java
│ │ │ │ ├── ConfigurableBeanFactory.java(接口) 定义了 destroySingletons 销毁方法
│ │ │ │ └── SingletonBeanRegistry.java
│ │ │ ├── support
│ │ │ │ ├── AbstractAutowireCapableBeanFactory.java(抽象类)

主要作用:
继承关系:继承AbstractBeanFactory
实现AutowireCapableBeanFactory接口
主要方法:
CreateBean():创建Bean 调用registerDisposableBeanIfNecessary
initializeBean():初始化Bean,调用PostProcessor Before 处理,执行初始化方法invokeInitMethods,执行 BeanPostProcessor After 处理

│ │ │ │ ├── AbstractBeanDefinitionReader.java
│ │ │ │ ├── AbstractBeanFactory.java
│ │ │ │ ├── BeanDefinitionReader.java
│ │ │ │ ├── BeanDefinitionRegistry.java
│ │ │ │ ├── CglibSubclassingInstantiationStrategy.java
│ │ │ │ ├── DefaultListableBeanFactory.java
│ │ │ │ ├── DefaultSingletonBeanRegistry.java

实现destroySingletons 销毁方法( AbstractBeanFactory.java的父类)

│ │ │ │ ├── DisposableBeanAdapter.java

描述:销毁方法适配器
继承关系: 实现DisposableBean接口

│ │ │ │ ├── FactoryBeanRegistrySupport.java(继承 DefaultSingletonBeanRegistry)

作用:实现一个 FactoryBean 注册服务
维护一个存放FactoryBean对象的缓存 factoryBeanObjectCache
处理的就是关于 FactoryBean 此类对象的注册操作
GetObjectFromFactoryBean() 从FactoryBean通过getObject()获取对象,先判断缓存中是否有 如果有直接获取,没有则加入缓存

│ │ │ │ ├── InstantiationStrategy.java
│ │ │ │ └── SimpleInstantiationStrategy.java
│ │ │ ├── support
│ │ │ │ └── XmlBeanDefinitionReader.java
│ │ │ ├── Aware.java(接口)
│ │ │ ├── BeanClassLoaderAware.java(实现Aware接口)
│ │ │ ├── BeanFactory.java
│ │ │ ├──BeanFactoryAware.java(实现Aware接口)
│ │ │ ├── BeanNameAware.java
│ │ │ ├── ConfigurableListableBeanFactory.java
│ │ │ ├── DisposableBean.java
│ │ │ ├──FactoryBean.java(实现FactoryBean)

主要方法:
getObject()获取对象
getObjectType()对象类型
isSingleton()是否是单例对象 如果是单例对象会被放到内存中

│ │ │ ├── HierarchicalBeanFactory.java
│ │ │ ├── InitializingBean.java(接口) 定义初始化方法
│ │ │ └── ListableBeanFactory.java
│ │ ├── BeansException.java
│ │ ├── PropertyValue.java
│ │ └── PropertyValues.java
│ ├── context
│ │ ├── support
│ │ │ ├── AbstractApplicationEventMulticaster.java

继承 ApplicationEventMulticaster的抽象类
维护一个事件监听器的Set,一个BeanFactory的属性
addApplicationListener() removeApplicationListener 添加和删除事件监听器
getApplicationListeners 摘取符合广播事件中的监听处理器 使用supportEvent判断
supportsEvent(applicationListener, ApplicationEvent event)

│ │ │ ├── ApplicationContextEvent.java (继承Application Event)

定义事件的抽象类

│ │ │ ├── ApplicationEventMulticaster.java

作用:添加监听和删除监听
接口

│ │ │ ├── ContextClosedEvent.java

定义关闭事件的抽象类(对象是ApplicationContent)

│ │ │ ├── ContextRefreshedEvent.java

定义刷新事件的抽象类(对象是ApplicationContent)

│ │ │ ├── SimpleApplicationEventMulticaster.java
│ │ ├── support
│ │ │ ├── AbstractApplicationContext.java(抽象类)

继承关系:实现 ConfigurableApplicationContext接口 继承DefaultResourceLoader类

│ │ │ ├── AbstractRefreshableApplicationContext.java
│ │ │ ├── AbstractXmlApplicationContext.java
│ │ │ ├── ApplicationContextAwareProcessor.java(实现BeanPostProcessor接口)
│ │ │ └── ClassPathXmlApplicationContext.java
│ │ ├── ApplicationContext.java
│ │ ├──ApplicationContextAware.java
│ │ ├──ApplicationEvent.java

定义事件的抽象类
包含事件源对象

│ │ ├── ApplicationEventPublisher.java

事件发布接口

│ │ ├── ApplicationListener.java
│ │ └── ConfigurableApplicationContext.java(接口)

主要描述:虚拟机关闭钩子注册调用销毁,定义刷新容器,关闭应用上下文
继承关系:继承ApplicationContext
主要方法:
refresh():
registerShutdownHook():注册虚拟机钩子的方法
close():手动执行关闭虚拟机钩子的方法

│ ├── core.io
│ │ ├── ClassPathResource.java
│ │ ├── DefaultResourceLoader.java(实体类)

作用:资源处理器

│ │ ├── FileSystemResource.java
│ │ ├── Resource.java
│ │ ├── ResourceLoader.java
│ │ └── UrlResource.java
│ └── utils
│ └── ClassUtils.java
└── test

类图

在这里插入图片描述
整个类关系图中可以看到,在以 BeanPostProcessor 接口实现继承的 InstantiationAwareBeanPostProcessor 接口后,做了一个自动代理创建的类 DefaultAdvisorAutoProxyCreator,这个类的就是用于处理整个 AOP 代理融入到 Bean 生命周期中的核心类。
DefaultAdvisorAutoProxyCreator 会依赖于拦截器、代理工厂和Pointcut与Advisor的包装服务 AspectJExpressionPointcutAdvisor,由它提供切面、拦截方法和表达式。
Spring 的 AOP 把 Advice 细化了 BeforeAdvice、AfterAdvice、AfterReturningAdvice、ThrowsAdvice,目前我们做的测试案例中只用到了 BeforeAdvice,这部分可以对照 Spring 的源码进行补充测试。

定义Advice拦截器链

BeforeAdvice

public interface BeforeAdvice extends Advice {

}

MethodBeforeAdvice

public interface MethodBeforeAdvice extends BeforeAdvice {
    void before(Method method, Object[] args, Object target) throws Throwable;

}

在 Spring 框架中,Advice 都是通过方法拦截器 MethodInterceptor 实现的。环绕 Advice 类似一个拦截器的链路

定义 Advisor 访问者

aop.Advisor

public interface Advisor {

    Advice getAdvice();

}

aop.PointcutAdvisor

public interface PointcutAdvisor extends Advisor {
    Pointcut getPointcut();
}

Advisor 承担了 Pointcut 和 Advice 的组合,Pointcut 用于获取 JoinPoint,而 Advice 决定于 JoinPoint 执行什么操作。

AspectJExpressionPointcutAdvisor

public class AspectJExpressionPointcutAdvisor implements PointcutAdvisor {

    // 切面
    private AspectJExpressionPointcut pointcut;
    // 具体的拦截方法
    private Advice advice;
    // 表达式
    private String expression;

    public void setExpression(String expression){
        this.expression = expression;
    }
	//单例模式
    @Override
    public Pointcut getPointcut() {
        if (null == pointcut) {
            pointcut = new AspectJExpressionPointcut(expression);
        }
        return pointcut;
    }

    @Override
    public Advice getAdvice() {
        return advice;
    }

    public void setAdvice(Advice advice){
        this.advice = advice;
    }

}

AspectJExpressionPointcutAdvisor 实现了 PointcutAdvisor 接口,把切面 pointcut、拦截方法 advice 和具体的拦截表达式包装在一起。这样就可以在 xml 的配置中定义一个 pointcutAdvisor 切面拦截器了

方法拦截器

MethodBeforeAdviceInterceptor

public class MethodBeforeAdviceInterceptor implements MethodInterceptor {

    private MethodBeforeAdvice advice;

    public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) {
        this.advice = advice;
    }
	//这里用到反射
	//Invocation为方法执行器
	//ReflectiveMethodInvocation 
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        this.advice.before(methodInvocation.getMethod(), methodInvocation.getArguments(), methodInvocation.getThis());
        return methodInvocation.proceed();//是本来方法的调用
    }

}

MethodBeforeAdviceInterceptor 实现了 MethodInterceptor 接口,在 invoke 方法中调用 advice 中的 before 方法,传入对应的参数信息。

代理工厂

aop.framework.ProxyFactory

public class ProxyFactory {

    private AdvisedSupport advisedSupport;

    public ProxyFactory(AdvisedSupport advisedSupport) {
        this.advisedSupport = advisedSupport;
    }

    public Object getProxy() {
        return createAopProxy().getProxy();
    }

    private AopProxy createAopProxy() {
        if (advisedSupport.isProxyTargetClass()) {
            return new Cglib2AopProxy(advisedSupport);
        }

        return new JdkDynamicAopProxy(advisedSupport);
    }

}

代理工厂主要解决的是关于 JDK 和 Cglib 两种代理的选择问题,有了代理工厂就可以按照不同的创建需求进行控制。

融入Bean生命周期的自动代理创建者

aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator

public class DefaultAdvisorAutoProxyCreator implements InstantiationAwareBeanPostProcessor, BeanFactoryAware {

    private DefaultListableBeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = (DefaultListableBeanFactory) beanFactory;
    }

    @Override
    public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {

        if (isInfrastructureClass(beanClass)) return null;

        Collection<AspectJExpressionPointcutAdvisor> advisors = beanFactory.getBeansOfType(AspectJExpressionPointcutAdvisor.class).values();

        for (AspectJExpressionPointcutAdvisor advisor : advisors) {
            ClassFilter classFilter = advisor.getPointcut().getClassFilter();
            if (!classFilter.matches(beanClass)) continue;

            AdvisedSupport advisedSupport = new AdvisedSupport();

            TargetSource targetSource = null;
            try {
                targetSource = new TargetSource(beanClass.getDeclaredConstructor().newInstance());
            } catch (Exception e) {
                e.printStackTrace();
            }
            advisedSupport.setTargetSource(targetSource);
            advisedSupport.setMethodInterceptor((MethodInterceptor) advisor.getAdvice());
            advisedSupport.setMethodMatcher(advisor.getPointcut().getMethodMatcher());
            advisedSupport.setProxyTargetClass(false);

            return new ProxyFactory(advisedSupport).getProxy();

        }

        return null;
    }
    
}

核心类,主要核心实现在于 postProcessBeforeInstantiation 方法中,从通过 beanFactory.getBeansOfType 获取 AspectJExpressionPointcutAdvisor 开始。
获取了 advisors 以后就可以遍历相应的 AspectJExpressionPointcutAdvisor 填充对应的属性信息,包括:目标对象、拦截方法、匹配器,之后返回代理对象即可。
那么现在调用方获取到的这个 Bean 对象就是一个已经被切面注入的对象了,当调用方法的时候,则会被按需拦截,处理用户需要的信息。

测试

public class UserServiceBeforeAdvice implements MethodBeforeAdvice {

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("拦截方法:" + method.getName());
    }

}

xml

<beans>

    <bean id="userService" class="cn.bugstack.springframework.test.bean.UserService"/>

    <bean class="cn.bugstack.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>

    <bean id="beforeAdvice" class="cn.bugstack.springframework.test.bean.UserServiceBeforeAdvice"/>

    <bean id="methodInterceptor" class="cn.bugstack.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor">
        <property name="advice" ref="beforeAdvice"/>
    </bean>

    <bean id="pointcutAdvisor" class="cn.bugstack.springframework.aop.aspectj.AspectJExpressionPointcutAdvisor">
        <property name="expression" value="execution(* cn.bugstack.springframework.test.bean.IUserService.*(..))"/>
        <property name="advice" ref="methodInterceptor"/>
    </bean>

</beans>
@Test
public void test_aop() {
    ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring.xml");
    IUserService userService = applicationContext.getBean("userService", IUserService.class);
    System.out.println("测试结果:" + userService.queryUserInfo());
}

面试题

拦截器与切面的区别
什么是拦截器
什么是切面

是什么拦截器链路

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/439463.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

c++ 中const

对于基础类型直接赋值 void test01(){const int data10;cout<<"data"<<data<<endl;int * p (int*)&data;*p 1000;cout<<"*p"<<*p<<endl;cout<<"after data"<<data; } c中&#xff0c;对于…

Github 2024-03-02 开源项目日报Top9

根据Github Trendings的统计&#xff0c;今日(2024-03-02统计)共有9个项目上榜。根据开发语言中项目的数量&#xff0c;汇总情况如下&#xff1a; 开发语言项目数量非开发语言项目2Rust项目1JavaScript项目1Shell项目1C项目1TypeScript项目1C#项目1Python项目1 任天堂Switch模…

47. 【Linux教程】逻辑卷的简单使用

本小节介绍创建 LVM 的步骤&#xff0c;并介绍如何简单的使用 LVM&#xff0c;主要分为&#xff1a;定义物理卷、创建卷组、创建逻辑卷、创建文件系统、修改 LVM。 1.LVM 中的几个概念 PV(physical volume)&#xff0c;物理卷在逻辑卷管理系统最底层&#xff0c;可为整个物理硬…

那些像白牌的品牌正在霸榜TikTok美区!国货之光闪耀海外!

北京时间 3 月 6 日&#xff0c;据路透社报道&#xff0c;美国两党国会议员在周二提出了一项法案&#xff0c;要求字节跳动公司剥离对旗下短视频应用 TikTok 的控制权&#xff0c;否则就禁止应用商店上架分发 TikTok。 TikTok 对此回应称&#xff1a;“无论提案人如何掩饰&…

在线免费预览查看 Axure rp 原型

Axure RP 不仅可以绘制详细的产品概念&#xff0c;还可以在浏览器中生成 html 页面进行参考&#xff0c;但需要安装插件才能打开。安装 Axure rpchrome 插件之后&#xff0c;还需要在扩展程序中选择 “允许访问文件网站”&#xff0c;否则无法在 Axure 中成功。 在线查看原型。…

基于深度学习的驾驶员分心驾驶行为(疲劳+危险行为)预警系统使用YOLOv5+Deepsort实现驾驶员的危险驾驶行为的预警监测

人物专注性检测 项目快速预览 主要不同地方为&#xff1a; 1、疲劳检测中去掉了点头行为的检测&#xff0c;仅保留闭眼检测和打哈欠检测。 2、Yolov5的权重进行了重新训练&#xff0c;增加了训练轮次。 3、前端UI进行了修改&#xff0c;精简了部分功能。 项目介绍 该项目…

0基础学习VR全景平台篇第143篇:限定访问功能

大家好&#xff0c;欢迎观看蛙色VR官方——后台使用系列课程&#xff01;这期&#xff0c;我们将为大家介绍如何使用限定访问功能。 一.什么是限定访问功能&#xff1f; 限定访问&#xff0c;就是可以在编辑后台设置可以访问作品的用户的类型&#xff0c;还有可以访问作品的IP…

某准网招聘接口逆向之WebPack扣取

​​​​​逆向网址 aHR0cHM6Ly93d3cua2Fuemh1bi5jb20v 逆向链接 aHR0cHM6Ly93d3cua2Fuemh1bi5jb20vc2VhcmNoP3BhZ2VOdW09MSZxdWVyeT1weXRob24mdHlwZT01 逆向接口 aHR0cHM6Ly93d3cua2Fuemh1bi5jb20vYXBpX3RvL3NlYXJjaC9qb2IuanNvbg 逆向过程 请求方式&#xff1a;GET 参数构成…

434G数据失窃!亚信安全发布《勒索家族和勒索事件监控报告》

最新态势快速感知 最新一周全球共监测到勒索事件90起&#xff0c;与上周相比数量有所增加。 lockbit3.0仍然是影响最严重的勒索家族&#xff1b;alphv和cactus恶意家族也是两个活动频繁的恶意家族&#xff0c;需要注意防范。 Change Healthcare - Optum - UnitedHealth遭受了…

vue实现文字手工动态打出效果

vue实现文字手工动态打出效果 问题背景 本文实现vue中&#xff0c;动态生成文字手动打出效果。 问题分析 话不多说&#xff0c;直接上代码&#xff1a; <template><main><button click"makeText"><p class"text">点击生成内容…

【吊打面试官系列】Java虚拟机JVM篇 - 三道最简单最常问的JVM面试题

大家好&#xff0c;我是锋哥。今天分享三道最简单最常问的JVM面试题&#xff0c;希望对大家有帮助&#xff1b; 一&#xff0c;请问JDK与JVM有什么区别&#xff1f; 简单来说&#xff1a; 1. JVMJava 运行器&#xff1b; 2. JREJVM Java 基础&核心类库&#xff1b; 3. JD…

Java二级--操作题详解(1)

目录 1.第一套&#xff1a; 1.1 基本操作&#xff1a; 1.2 题解分析&#xff1a; 2.1 简单应用&#xff1a; 2.2 解题分析&#xff1a; 3.1 综合应用&#xff1a; 3.2解题分析&#xff1a; 1.第一套&#xff1a; 1.1 基本操作&#xff1a; 在考生文件夹中存有文件名为J…

Spring Boot工程集成验证码生成与验证功能教程

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

基于springboot的大学生智能消费记账系统的设计与实现(程序+数据库+文档)

** &#x1f345;点赞收藏关注 → 私信领取本源代码、数据库&#x1f345; 本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路。&#x1f345;关注我不迷路&#x1f345;** 一、研究背景…

1688平台上如何高效一键发布商品?如何接入1688API官方商品

1688平台上发布商品&#xff0c;现在很有优势。很多商品可以直接对接海外&#xff01; 1688平台发布商品的优势在1688平台发布商品的优势主要包括&#xff1a; 产品种类丰富&#xff1a;1688作为国内最大的B2B电商平台&#xff0c;提供的产品种类繁多&#xff0c;覆盖了各个行…

345.反转字符串中的元音字母

题目&#xff1a;给你一个字符串 s &#xff0c;仅反转字符串中的所有元音字母&#xff0c;并返回结果字符串。 元音字母包括 a、e、i、o、u&#xff0c;且可能以大小写两种形式出现不止一次。 class Solution {//画图&#xff0c;好理解点public String reverseVowels(String…

测试点点延迟和带宽的脚本总结

从队列中获取节点名 我们有时候需要从任务队列中取出完整的节点名称&#xff0c;比如cn[8044-8046,8358-8360,8926-8928,9002-9004]&#xff0c;可以给定参数input_str也可以在脚本中直接写死。 import re import subprocess import sysinput_str "cn[7512-7519,7545-75…

尚硅谷SpringBoot3笔记

推荐课程&#xff1a;03.快速入门-示例Demo_哔哩哔哩_bilibili 目录 01--示例demo 01--示例demo 1、在新建项目创建一个Maven 模块 2、引入 spring-boot-starter-parent 和 spring-boot-starter-web 依赖 spring-boot-starter-parent 是 Spring Boot 提供的一个用于构建 Spr…

爬虫实战——巴黎圣母院新闻【内附超详细教程,你上你也行】

文章目录 发现宝藏一、 目标二、简单分析网页1. 寻找所有新闻2. 分析模块、版面和文章 三、爬取新闻1. 爬取模块2. 爬取版面3. 爬取文章 四、完整代码五、效果展示 发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不…

四川宏博蓬达法律咨询:专业领航,法治路上的坚实后盾

在法治社会中&#xff0c;法律咨询服务扮演着举足轻重的角色。四川宏博蓬达法律咨询&#xff0c;作为业界的佼佼者&#xff0c;以其正规可靠的服务赢得了广大客户的信赖和好评。今天&#xff0c;我们就来一起了解一下这家在法律服务领域备受赞誉的企业。 一、正规资质&#xff…