Springboot学习笔记

SpringBoot学习笔记

1、SpringBoot核心注解

 // 本质是一个配置类,也是一个组件
 @SpringBootConfiguration
    @Configuration
  @Component
 
 // 自动配置类
 @EnableAutoConfiguration
   @AutoConfigurationPackage // 自动配置包
      @Import(AutoConfigurationPackages.Registrar.class) // 导入选择器‘包注册’
   @Import(AutoConfigurationImportSelector.class) // 自动配置导入选择
 
 // 获取所有的配置
 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);

获取候选的配置:

 protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
  List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
  getBeanClassLoader());
  Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
  + "are using a custom packaging, make sure that file is correct.");
  return configurations;
  }

 

自动配置的核心文件:

 META-INF/spring.factories

 

结论:springboot所有的自动配置都在启动的时候被扫描并加载:spring.factories,所有的自动配置类都在这个文件,配置类是否会生效,这就要判断条件是否成立。也就是是否导入了对应的start(启动器),springboot的依赖基本都是start启动器。

 

2、yaml配置文件的格式

1、基本格式

 # 普通的key-value形式
 name: zhangsan
 
 # 对象的形式
 student:
  name: zhangsan
  age: 21
  sex: 男
 
 # 对象的行内写法
 student: {name: lisi,age: 22, sex: 女}
 
 # 数组
 pets:
  - dog
  - cat
  - pig
 
 pets: [dog,cat,pig]

 

2、使用@ConfigurationProperties注解,通过yaml配置数据给实体类赋值

 person:
  name: 张三
  age: 22
  happy: true
  hobby:
    - game
    - code
    - girl
  map: {k1: v1,k2: v2}
  dog:
    name: 大黄
    age: 2
 @ConfigurationProperties(prefix = "person")
 @ToString
 @Data
 @Component
 public class Person {
     private String name;
 
     private int age;
 
     private boolean happy;
 
     private List<String> hobby;
 
     private Map<String,String> map;
 
     private Dog dog;
 }

 

3、@ConfigurationProperties的作用:可以注解的prefix属性指定配置的前缀,让实体类跟配置绑定,并把配置数据自动映射到实体类中,在我们的实际开发中,可以发现,我们的配置类都是这么玩的。

 

4、PropertySource(value = "properties属性文件的路径") + @Value :在实体类上使用了这个注解,可以让我们的实体类绑定属性文件,在属性上用@Value注解从而让属性文件里面的值映射到实体类的属性中。

 

自行拓展:在我们用yaml配置文件给实体类赋值的时候,在yaml配置中我们还可以用EL表达式

 

3、JSR303校验,类上使用@Validated注解(自行拓展)

 空检查
 @Null       验证对象是否为null
 @NotNull   验证对象是否不为null, 无法查检长度为0的字符串
 @NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
 @NotEmpty 检查约束元素是否为NULL或者是EMPTY.
 
 Booelan检查
 @AssertTrue     验证 Boolean 对象是否为 true  
 @AssertFalse   验证 Boolean 对象是否为 false  
   
 长度检查
 @Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内  
 @Length(min=, max=) Validates that the annotated string is between min and max included.
 
 日期检查
 @Past           验证 Date 和 Calendar 对象是否在当前时间之前  
 @Future     验证 Date 和 Calendar 对象是否在当前时间之后  
 @Pattern   验证 String 对象是否符合正则表达式的规则
 
 数值检查,建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为"",Integer为null
 @Min           验证 Number 和 String 对象是否大等于指定的值  
 @Max           验证 Number 和 String 对象是否小等于指定的值  
 @DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
 @DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
 @Digits     验证 Number 和 String 的构成是否合法  
 @Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。
   
 @Range(min=, max=) 检查数字是否介于min和max之间.
 @Range(min=10000,max=50000,message="range.bean.wage")
 private BigDecimal wage;
 
 @Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证)
 @CreditCardNumber信用卡验证
 @Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。
 @ScriptAssert(lang= ,script=, alias=)
 @URL(protocol=,host=, port=,regexp=, flags=)

 

4、多个配置文件的切换和优先级

1、多个文件的切换

在我们的实际开发过程中一般会有多个配置文件(生产环境、测试环境),我们可以使用spring.profiles.active=?来激活需要的配置文件,'?'代表需要激活的配置文件

  • 主配置文件:application.yaml,我们激活开发环境的配置文件

    注意:在yaml文件中我们可以使用---来区分多个文件

     server:
      port: 8081
     
     spring:
      profiles:
        active: test
     
     ---
     server:
      port: 8082
     spring:
      profiles: dev
     
     ---
     server:
      port: 8083
     spring:
      profiles: test

当我们同时存在着三个配置文件并启动项目的时候,我们会发现我们项目的端口用的是8082,这就是因为我们激活的是开发环境的配置文件。

 

2、项目中配置文件的优先级

  1. spring boot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件  

    –file:./config/   –file:./   –classpath:/config/   –classpath:/

     

    以上是按照优先级从高到低的顺序,所有位置的文件都会被加载,高优先级配置内容会覆盖低优先级配置内容。

    SpringBoot会从这四个位置全部加载主配置文件,如果高优先级中配置文件属性与低优先级配置文件不冲突的属性,则会共同存在—互补配置。

     

    我们也可以通过配置spring.config.location来改变默认配置。

    java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar --spring.config.location=file:///D:/application.properties,classpath:/,classpath:/config/ 项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置。

    指定配置文件和默认加载的这些配置文件共同起作用形成互补配置。 Idea 单测启用自定义配置:添加jvm参数:-Dspring.config.location=file:///D:/project_conf/application.yml -ea

 

  1. 外部配置加载顺序

    SpringBoot也可以从以下位置加载配置:优先级从高到低;高优先级的配置覆盖低优先级的配置,所有的配置会形成互补配置。

    1.命令行参数

    • 所有的配置都可以在命令行上进行指定;

    • 多个配置用空格分开; –配置项=值

      java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar --server.port=8087 --server.context-path=/abc

    2.来自java:comp/env的JNDI属性

    3.Java系统属性(System.getProperties())

    4.操作系统环境变量

    5.RandomValuePropertySource配置的random.*属性值

    6.jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置文件

    7.jar包内部的application-{profile}.properties或application.yml(带spring.profile)配置文件

    8.jar包外部的application.properties或application.yml(不带spring.profile)配置文件

    9.jar包内部的application.properties或application.yml(不带spring.profile)配置文件

    10.@Configuration注解类上的@PropertySource

    11.通过SpringApplication.setDefaultProperties指定的默认属性

 

5、SpringBoot的@ConditionOnxxx注解的总结:

@ConditionOnxxx注解在SpringBoot的自动配置中至关重要,在SpringBoot的配置类中我们常常能看到类似@ConditionOnxxx的注解,这些注解能决定配置类是否生效,以下就是关于该类注解的用法和解释:

  • @Conditional(TestCondition.class):这句代码可以标注在类上面,表示该类下面的所有@Bean都会启用配置,也可以标注在方法上面,只是对该方法启用配置。

  • @ConditionalOnBean(仅仅在当前上下文中存在某个对象时,才会实例化一个Bean)

  • @ConditionalOnClass(某个class位于类路径上,才会实例化一个Bean)

  • @ConditionalOnExpression(当表达式为true的时候,才会实例化一个Bean)

  • @ConditionalOnMissingBean(仅仅在当前上下文中不存在某个对象时,才会实例化一个Bean)

  • @ConditionalOnMissingClass(某个class类路径上不存在的时候,才会实例化一个Bean)

  • @ConditionalOnNotWebApplication(不是web应用)

 

另一种总结

  • @ConditionalOnClass:该注解的参数对应的类必须存在,否则不解析该注解修饰的配置类;

  • @ConditionalOnMissingBean:该注解表示,如果存在它修饰的类的bean,则不需要再创建这个bean;可以给该注解传入参数例如

  • @ConditionOnMissingBean(name = "example"),这个表示如果name为“example”的bean存在,这该注解修饰的代码块不执行。

 

6、自动配置的总结

每个自动配置类(xxxAutoConfiguration.java)都会绑定一个xxxProperties.java类,而每个xxxProperties.java都会通过@ConfigurationProperties(prefix = "xxx")绑定配置文件中的配置,因而我们就可以在配置文件中手动配置自定义的一些属性值。

 

7、静态资源访问问题

1、在mvc的自动配置类中:WebMvcAutoConfiguration.java,以下方法说明了能识别静态资源的位置

 @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
  if (!this.resourceProperties.isAddMappings()) {
  logger.debug("Default resource handling disabled");
  return;
  }
  addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
  addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
  registration.addResourceLocations(this.resourceProperties.getStaticLocations());
  if (this.servletContext != null) {
  ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
  registration.addResourceLocations(resource);
  }
  });
  }
 private String staticPathPattern = "/**";
 
 public String getStaticPathPattern() {
  return this.staticPathPattern;
  }
 private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
  "classpath:/resources/", "classpath:/static/", "classpath:/public/" };
 
 private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
 
 public String[] getStaticLocations() {
  return this.staticLocations;
  }

 

总结:我们可以在以下目录放置我们的静态资源:

  • webjars

  • public

  • static

  • resources

  • /**

优先级:reources > static > public

 

8、全面接管SpringMVC

要定制自己的SpringMVC,只需要写一个配置类继承WebMvcConfigurer接口,需要定制哪些功能,就重写接口的方法。如果是全面接管SpringMVC,只需在自定义配置类上加上@EnableWebMvc注解(不建议),如果只是部分接管,就不要加@EnableWebMvc注解。

 @Configuration
 public class MyWebMvcConfig implements WebMvcConfigurer {
 
     /**
      * 自定义视图解析器
      * @return
      */
     @Bean
     public ViewResolver getMyViewResolver(){
         return new MyViewResolver();
    }
 
     public static class MyViewResolver implements ViewResolver{
         @Override
         public View resolveViewName(String viewName, Locale locale) throws Exception {
             return null;
        }
    }
 
     /**
      * 自定义视图跳转
      * @param registry
      */
     @Override
     public void addViewControllers(ViewControllerRegistry registry) {
         registry.addViewController("/index").setViewName("index");
    }
 }

 

 

9、Druid数据源配置

  1. 导入jar包

     <!--druid数据源-->
     <dependency>
       <groupId>com.alibaba</groupId>
       <artifactId>druid-spring-boot-starter</artifactId>
       <version>1.2.4</version>
     </dependency>
     
     <!--log4j-->
     <dependency>
       <groupId>log4j</groupId>
       <artifactId>log4j</artifactId>
       <version>1.2.17</version>
     </dependency>
  2. yaml配置

      # 使用druid数据源
        type: com.alibaba.druid.pool.DruidDataSource
         # 初始化物理连接个数
        initialSize: 0
         # 最大连接池数量
        maxActive: 20
         # 最小连接池数量
        minIdle: 1
         # 最长等待时间
        maxWait: 60000
         # 配置插件
        filters: stat,log4j,wall
  3. druid配置类

     @Configuration
     public class DruidConfig {
     
         /**
          * 把druid数据源交由spring容器管理
          * @return
          */
         @Bean
         @ConfigurationProperties(prefix = "spring.datasource")
         public DruidDataSource druidDataSource(){
             DruidDataSource druidDataSource = new DruidDataSource();
             return druidDataSource;
        }
     
         /**
          * 数据源sql监控
          * @return
          */
         @Bean
         public ServletRegistrationBean statViewServlet(){
             ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(),"/druid/*");
     
             // 初始化参数
             Map<String,String> initParams = new HashMap<>();
             // 设置账户和密码访问
             initParams.put("loginUsername","admin");
             initParams.put("loginPassword","admin123");
             // 允许哪台主机访问
             initParams.put("allow","");
     
             bean.setInitParameters(initParams);
             return bean;
        }
     
         /**
          * 对哪些请求进行监控
          * @return
          */
         @Bean
         public FilterRegistrationBean filterRegistrationBean(){
             FilterRegistrationBean bean = new FilterRegistrationBean(new WebStatFilter());
     
             // 初始化参数
             Map<String,String> initParams = new HashMap<>();
             // 对所有的请求进行监控
             initParams.put("urlPatterns","/*");
             // 不包含静态资源的请求
             initParams.put("exclusions","/*.js,/*.gif,/*.jpg,/*.png,/*.css,/druid/*");
     
             bean.setInitParameters(initParams);
             return bean;
        }
     }

     

10、SpringSecurity的简单使用

1、导入jar包

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-security</artifactId>
     <version>2.5.0</version>
 </dependency>

2、配置类配置(需要继承)

 @EnableWebSecurity
 public class MySpringSecurity extends WebSecurityConfigurerAdapter {
 
     /**
      * 授权
      * @param http
      * @throws Exception
      */
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         // 设置首页允许所有访问,其它的需要对应的权限
         http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/students").hasRole("vip1")
                .antMatchers("/hello/**").hasRole("vip2");
 
         // 没有权限会到登录页面
         http.formLogin();
         // 开启注销功能(前端调用"/logout"便可注销)
         http.logout();
    }
 
     /**
      * 认证
      * 在 spring security 5.0+中增加了很多加密方法
      * @param auth
      * @throws Exception
      */
     @Override
     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         // 从内存虚拟一个用户
         auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
        .withUser("admin").password(newBCryptPasswordEncoder().encode("admin123")).roles("vip1","vip2")
                .and()
                .withUser("root").password(new BCryptPasswordEncoder().encode("root123")).roles("vip1");
    }
 }

 

11、集成Swagger

1、导入jar包

        <dependency>
             <groupId>io.springfox</groupId>
             <artifactId>springfox-swagger2</artifactId>
             <version>2.9.2</version>
         </dependency>
         
         <dependency>
             <groupId>io.springfox</groupId>
             <artifactId>springfox-swagger-ui</artifactId>
             <version>2.9.2</version>
         </dependency>

2、Swagger的配置

 @Configuration
 @EnableSwagger2 // 自动启用
 public class SwaggerConfig {
 
     @Bean
     public Docket docket(){
         return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                 // 配置接口扫描
                .apis(RequestHandlerSelectors.basePackage("com.example.springbootswagger"))
                 // 过滤哪些路径
                .paths(PathSelectors.ant("/hello/**"))
                .build();
    }
 
     private ApiInfo apiInfo(){
         // 作者信息
         Contact contact = new Contact("Mr.zhou", "baidu.com", "123@qq.com");
 
         return new ApiInfo("Mr.zhou的Api文档", "即使再小的帆也能远航", "V1.0", "https://www.baidu.com",
                 contact, "Apache 2.0", "http://www.apache.org/licenses/LICENSE-2.0", new ArrayList<VendorExtension>());
    }
 }

3、如果我们想在测试环境下开启swagger而不想在生产环境开启swagger,那么该怎么做呢?

 @Configuration
 @EnableSwagger2 // 自动启用
 public class SwaggerConfig {
 
     @Bean
     public Docket docket(Environment environment){
 
         // 设置要显示swagger的环境
         Profiles profiles = Profiles.of("dev","test");
         // 监听当前环境是否在设置的环境当中
         boolean flag = environment.acceptsProfiles(profiles);
 
         return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                 // 是否启用swagger
                .enable(flag)
                .select()
                 // 配置接口扫描
                .apis(RequestHandlerSelectors.basePackage("com.example.springbootswagger"))
                 // 过滤哪些路径
                .paths(PathSelectors.ant("/hello/**"))
                .build();
    }
 
     private ApiInfo apiInfo(){
         // 作者信息
         Contact contact = new Contact("Mr.zhou", "baidu.com", "123@qq.com");
 
         return new ApiInfo("Mr.zhou的Api文档", "即使再小的帆也能远航", "V1.0", "https://www.baidu.com",
                 contact, "Apache 2.0", "http://www.apache.org/licenses/LICENSE-2.0", new ArrayList<VendorExtension>());
    }
 }

 

12、任务

1、异步任务

  1. 在主配置类上加上@EnableAsync注解,开启异步任务

  2. 在我们需要异步执行的方法上加上@Async注解即可

 

2、定时任务

  1. TaskScheduler 任务调度者

  2. TaskExecutor 任务执行者

1、在主配置类上添加@EnableScheduling注解

2、在我们需要执行的方法前加@Scheduled(cron = "表达式")注解即可

3、邮件发送

1、添加依赖

 <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-thymeleaf</artifactId>
 </dependency>

2、根据源码的配置文件进行配置

 # 邮箱服务器
 spring.mail.host=smtp.163.com
 # 邮箱名
 spring.mail.username=邮箱
 # 开启pop3时给的密码
 spring.mail.password=密码

3、测试

 @SpringBootTest
 class SpringbootSwaggerApplicationTests {
 
     @Autowired
     JavaMailSenderImpl javaMailSender;
 
     /**
      * 发送简单的邮件
      */
     @Test
     void SimpleSend(){
         // 简单的邮件
         SimpleMailMessage mail = new SimpleMailMessage();
         // 发送方
         mail.setFrom("zq_wabc2020@163.com");
         // 接收方
         mail.setTo("568390181@qq.com");
         // 邮件主题
         mail.setSubject("发给自己的测试邮件");
         // 发送内容
         mail.setText("Hello World");
         javaMailSender.send(mail);
    }
 
     /**
      * 发送一个复杂的邮件
      * @throws MessagingException
      */
     @Test
     void complexSend() throws MessagingException {
         // 一个复杂的邮件
         MimeMessage message = javaMailSender.createMimeMessage();
         // 组装
         MimeMessageHelper helper = new MimeMessageHelper(message,true);
         // 发送方
         helper.setFrom("zq_wabc2020@163.com");
         // 接收方
         helper.setTo("568390181@qq.com");
         // 邮件主题
         helper.setSubject("发给自己的复杂邮件");
         // 文本
         helper.setText("<p style='color:red'>送你两张图片</p>",true);
 
         // 发送附件
         helper.addAttachment("图片1.jpeg",new File("E:/picture/picture1.jpeg"));
         helper.addAttachment("图片2.jpeg",new File("E:/picture/picture9.jpeg"));
         javaMailSender.send(message);
    }
 }

 

13、集成Redis

说明:在SpringBoot2.x之后Jedis被替换成了lettuce

Jedis:底层采用的直连,多个线程操作的话是不安全的,要想避免,就要采用jedis pool连接池,想BIO模式

lettuce:采用的是netty,实例可以在多个线程中共享,不存在线程不安全的情况!可以减少线程数量,更像NIO模式

源码分析:

 public class RedisAutoConfiguration {
 
  @Bean
     // 当redisTemplate这个类不存在才生效,也就是说我们可以自己定义一个
  @ConditionalOnMissingBean(name = "redisTemplate")
  @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
  public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
         // 默认的RedisTemplate没有过多的配置,而redis对象是需要序列化的
         // 默认类型是RedisTemplate<Object, Object> ,而我们希望用到的类型是RedisTemplate<String, Object>
  RedisTemplate<Object, Object> template = new RedisTemplate<>();
  template.setConnectionFactory(redisConnectionFactory);
  return template;
  }
 
     // 这里单独配置了一个String类型的模板,因为我们最常用的就是String类型
  @Bean
  @ConditionalOnMissingBean
  @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
  public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
  StringRedisTemplate template = new StringRedisTemplate();
  template.setConnectionFactory(redisConnectionFactory);
  return template;
  }
 
 }

1、配置

 spring:
  redis:
    host: 主机
    port: 6379

注意:centos7.6关闭防火墙相关命令

 启动一个服务:systemctl start firewalld.service
 关闭一个服务:systemctl stop firewalld.service
 重启一个服务:systemctl restart firewalld.service
 显示一个服务的状态:systemctl status firewalld.service
 在开机时启用一个服务:systemctl enable firewalld.service
 在开机时禁用一个服务:systemctl disable firewalld.service
 查看服务是否开机启动:systemctl is-enabled firewalld.service;echo $?
 查看已启动的服务列表:systemctl list-unit-files|grep enabled 

2、自己配置RedisTemplate

 @Configuration
 public class RedisConfig {
 
     @Bean
     @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
     public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
         RedisTemplate<String, Object> template = new RedisTemplate<>();
 
         // 使用jackSon的序列化方式
         Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
         ObjectMapper om = new ObjectMapper();
         om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
         om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
         jackson2JsonRedisSerializer.setObjectMapper(om);
 
         // key采用String的序列方式
         StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
         template.setKeySerializer(stringRedisSerializer);
         // hash的key采用String的序列方式
         template.setHashKeySerializer(stringRedisSerializer);
         // value采用jackson的序列方式
         template.setValueSerializer(jackson2JsonRedisSerializer);
         // hash的value采用jackson的序列方式
         template.setHashValueSerializer(jackson2JsonRedisSerializer);
         template.afterPropertiesSet();
 
         template.setConnectionFactory(redisConnectionFactory);
         return template;
    }
 }
 

 

 

posted @   有梦想的程序员。  阅读(297)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
点击右上角即可分享
微信分享提示