全面升级!一套基于Spring Boot 3+JDK17的实战项目!

news2024/12/23 14:32:21

最近把mall项目升级支持了Spring Boot 3+JDK17,今天就来介绍下mall项目做了哪些升级,包括依赖的升级、框架的用法升级以及运行部署的改动,目前Spring Boot 3版本代码在mall项目的dev-v3分支下,希望对大家有所帮助!

mall项目简介

这里还是先简单介绍下mall项目吧,mall项目是一套基于 SpringBoot + Vue + uni-app 实现的电商系统(Github标星60K),采用Docker容器化部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!

  • 项目地址:https://github.com/macrozheng/mall
  • 项目文档:https://www.macrozheng.com

项目演示:

升级版本

目前项目中的依赖都已经升级到了最新主流版本,具体的版本可以参考下表。

框架版本说明
SpringBoot2.7.5->3.2.2Java应用开发框架
SpringSecurity5.7.4->6.2.1认证和授权框架
MyBatis3.5.10->3.5.14ORM框架
MyBatisGenerator1.4.1->1.4.2数据层代码生成器
SprngDataRedis2.7.5->3.2.2Redis数据操作框架
SprngDataElasticsearch4.4.5->5.2.2Elasticsearch数据操作框架
SprngDataMongoDB3.4.5->4.2.2MongoDB数据操作框架
Druid1.2.14->1.2.21数据库连接池
Hutool5.8.9->5.8.16Java工具类库
PageHelper5.3.2->6.1.0MyBatis物理分页插件
Swagger-UISpringFox->SpringDocAPI文档生成工具
logstash-logback-encoder7.2->7.4Logstash日志收集插件
docker-maven-plugin0.40.2->0.43.3应用打包成Docker镜像的Maven插件

升级用法

在mall项目升级Spring Boot 3的过程中,有些框架的用法有所改变,比如生成API文档的库改用了SpringDoc,Spring Data Elasticsearch和Spring Security随着版本升级,用法也不同了,这里我们将着重讲解这些升级的新用法!

从SpringFox迁移到SpringDoc

由于之前使用的Swagger库为SpringFox,目前已经不支持Spring Boot 3了,这里迁移到了SpringDoc。

  • 迁移到SpringDoc后,在application.yml需要添加SpringDoc的相关配置;
springdoc:
  swagger-ui:
    # 修改Swagger UI路径
    path: /swagger-ui.html
    # 开启Swagger UI界面
    enabled: true
    # 用于配置tag和operation的展开方式,这里配置为都不展开
    doc-expansion: 'none'
  api-docs:
    # 修改api-docs路径
    path: /v3/api-docs
    # 开启api-docs
    enabled: true
  group-configs:
    - group: 'default'
      packages-to-scan: com.macro.mall.controller
  default-flat-param-object: false
  • Java配置也需要做对应修改,具体参考SpringDocConfig配置类的代码;
/**
 * SpringDoc相关配置
 * Created by macro on 2024/3/5.
 */
@Configuration
public class SpringDocConfig implements WebMvcConfigurer {

    private static final String SECURITY_SCHEME_NAME = "Authorization";

    @Bean
    public OpenAPI mallAdminOpenAPI() {
        return new OpenAPI()
                .info(new Info().title("mall后台系统")
                        .description("mall后台相关接口文档")
                        .version("v1.0.0")
                        .license(new License().name("Apache 2.0")
                                .url("https://github.com/macrozheng/mall-learning")))
                .externalDocs(new ExternalDocumentation()
                        .description("SpringBoot实战电商项目mall(60K+Star)全套文档")
                        .url("http://www.macrozheng.com"))
                .addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME))
                .components(new Components()
                        .addSecuritySchemes(SECURITY_SCHEME_NAME,
                                new SecurityScheme()
                                        .name(SECURITY_SCHEME_NAME)
                                        .type(SecurityScheme.Type.HTTP)
                                        .scheme("bearer")
                                        .bearerFormat("JWT")));
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        //配置访问`/swagger-ui/`路径时可以直接跳转到`/swagger-ui/index.html`
        registry.addViewController("/swagger-ui/").setViewName("redirect:/swagger-ui/index.html");
    }

}
  • 之前在Controller和实体类上使用的SpringFox的注解,需要改用SpringDoc的注解,注解对照关系可以参考下表;
SpringFoxSpringDoc注解用途
@Api@Tag用于接口类,标识这个类是Swagger的资源,可用于给接口类添加说明
@ApiIgnore@Parameter(hidden = true)or@Operation(hidden = true)or@Hidden忽略该类的文档生成
@ApiImplicitParam@Parameter隐式指定接口方法中的参数,可给请求参数添加说明
@ApiImplicitParams@Parameters隐式指定接口方法中的参数集合,为上面注解的集合
@ApiModel@Schema用于实体类,声明一个Swagger的模型
@ApiModelProperty@Schema用于实体类的参数,声明Swagger模型的属性
@ApiOperation(value = “foo”, notes = “bar”)@Operation(summary = “foo”, description = “bar”)用于接口方法,标识这个类是Swagger的一个接口,可用于给接口添加说明
@ApiParam@Parameter用于接口方法参数,给请求参数添加说明
@ApiResponse(code = 404, message = “foo”)ApiResponse(responseCode = “404”, description = “foo”)用于描述一个可能的返回结果
  • 在我们使用SpringDoc生成的文档时,有一点需要特别注意,添加认证请求头时,已经无需添加Bearer 前缀,SpringDoc会自动帮我们添加的。

Spring Data Elasticsearch新用法

Spring Data ES中基于ElasticsearchRepository的一些简单查询的用法是没变化的,对于复杂查询,由于ElasticsearchRestTemplate类已经被移除,需要使用ElasticsearchTemplate类来实现。

  • 使用ElasticsearchTemplate实现的复杂查询,对比之前变化也不大,基本就是一些类和方法改了名字而已,大家可以自行参考EsProductServiceImpl类中源码即可;
/**
 * 搜索商品管理Service实现类
 * Created by macro on 2018/6/19.
 */
@Service
public class EsProductServiceImpl implements EsProductService {
    private static final Logger LOGGER = LoggerFactory.getLogger(EsProductServiceImpl.class);
    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;

    @Override
    public Page<EsProduct> search(String keyword, Long brandId, Long productCategoryId, Integer pageNum, Integer pageSize,Integer sort) {
        Pageable pageable = PageRequest.of(pageNum, pageSize);
        NativeQueryBuilder nativeQueryBuilder = new NativeQueryBuilder();
        //分页
        nativeQueryBuilder.withPageable(pageable);
        //过滤
        if (brandId != null || productCategoryId != null) {
            Query boolQuery = QueryBuilders.bool(builder -> {
                if (brandId != null) {
                    builder.must(QueryBuilders.term(b -> b.field("brandId").value(brandId)));
                }
                if (productCategoryId != null) {
                    builder.must(QueryBuilders.term(b -> b.field("productCategoryId").value(productCategoryId)));
                }
                return builder;
            });
            nativeQueryBuilder.withFilter(boolQuery);
        }
        //搜索
        if (StrUtil.isEmpty(keyword)) {
            nativeQueryBuilder.withQuery(QueryBuilders.matchAll(builder -> builder));
        } else {
            List<FunctionScore> functionScoreList = new ArrayList<>();
            functionScoreList.add(new FunctionScore.Builder()
                    .filter(QueryBuilders.match(builder -> builder.field("name").query(keyword)))
                    .weight(10.0)
                    .build());
            functionScoreList.add(new FunctionScore.Builder()
                    .filter(QueryBuilders.match(builder -> builder.field("subTitle").query(keyword)))
                    .weight(5.0)
                    .build());
            functionScoreList.add(new FunctionScore.Builder()
                    .filter(QueryBuilders.match(builder -> builder.field("keywords").query(keyword)))
                    .weight(2.0)
                    .build());
            FunctionScoreQuery.Builder functionScoreQueryBuilder = QueryBuilders.functionScore()
                    .functions(functionScoreList)
                    .scoreMode(FunctionScoreMode.Sum)
                    .minScore(2.0);
            nativeQueryBuilder.withQuery(builder -> builder.functionScore(functionScoreQueryBuilder.build()));
        }
        //排序
        if(sort==1){
            //按新品从新到旧
            nativeQueryBuilder.withSort(Sort.by(Sort.Order.desc("id")));
        }else if(sort==2){
            //按销量从高到低
            nativeQueryBuilder.withSort(Sort.by(Sort.Order.desc("sale")));
        }else if(sort==3){
            //按价格从低到高
            nativeQueryBuilder.withSort(Sort.by(Sort.Order.asc("price")));
        }else if(sort==4){
            //按价格从高到低
            nativeQueryBuilder.withSort(Sort.by(Sort.Order.desc("price")));
        }
        //按相关度
        nativeQueryBuilder.withSort(Sort.by(Sort.Order.desc("_score")));
        NativeQuery nativeQuery = nativeQueryBuilder.build();
        LOGGER.info("DSL:{}", nativeQuery.getQuery().toString());
        SearchHits<EsProduct> searchHits = elasticsearchTemplate.search(nativeQuery, EsProduct.class);
        if(searchHits.getTotalHits()<=0){
            return new PageImpl<>(ListUtil.empty(),pageable,0);
        }
        List<EsProduct> searchProductList = searchHits.stream().map(SearchHit::getContent).collect(Collectors.toList());
        return new PageImpl<>(searchProductList,pageable,searchHits.getTotalHits());
    }
}
  • 目前ES 7.17.3版本还是兼容的,这里测试了下ES 8.x版本,也是可以正常使用的,需要注意的是如果使用了8.x版本版本,对应的Kibana、Logstash和中文分词插件analysis-ik都需要使用8.x版本。

Spring Security新用法

升级Spring Boot 3版本后Spring Security的用法也有所变化,比如某些实现动态权限的类已经被弃用了,Security配置改用了函数式编程的方式。

  • 我们之前用于实现动态权限的DynamicAccessDecisionManager和DynamicSecurityFilter类实现的接口均已被弃用,取而代之的是需要实现AuthorizationManager接口;

  • 这里我们创建一个DynamicAuthorizationManager类来实现动态权限逻辑;
/**
 * 动态鉴权管理器,用于判断是否有资源的访问权限
 * Created by macro on 2023/11/3.
 */
public class DynamicAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {

    @Autowired
    private DynamicSecurityMetadataSource securityDataSource;
    @Autowired
    private IgnoreUrlsConfig ignoreUrlsConfig;

    @Override
    public void verify(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
        AuthorizationManager.super.verify(authentication, object);
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext requestAuthorizationContext) {
        HttpServletRequest request = requestAuthorizationContext.getRequest();
        String path = request.getRequestURI();
        PathMatcher pathMatcher = new AntPathMatcher();
        //白名单路径直接放行
        List<String> ignoreUrls = ignoreUrlsConfig.getUrls();
        for (String ignoreUrl : ignoreUrls) {
            if (pathMatcher.match(ignoreUrl, path)) {
                return new AuthorizationDecision(true);
            }
        }
        //对应跨域的预检请求直接放行
        if(request.getMethod().equals(HttpMethod.OPTIONS.name())){
            return new AuthorizationDecision(true);
        }
        //权限校验逻辑
        List<ConfigAttribute> configAttributeList = securityDataSource.getConfigAttributesWithPath(path);
        List<String> needAuthorities = configAttributeList.stream()
                .map(ConfigAttribute::getAttribute)
                .collect(Collectors.toList());
        Authentication currentAuth = authentication.get();
        //判定是否已经实现登录认证
        if(currentAuth.isAuthenticated()){
            Collection<? extends GrantedAuthority> grantedAuthorities = currentAuth.getAuthorities();
            List<? extends GrantedAuthority> hasAuth = grantedAuthorities.stream()
                    .filter(item -> needAuthorities.contains(item.getAuthority()))
                    .collect(Collectors.toList());
            if(CollUtil.isNotEmpty(hasAuth)){
                return new AuthorizationDecision(true);
            }else{
                return new AuthorizationDecision(false);
            }
        }else{
            return new AuthorizationDecision(false);
        }
    }
}
  • 然后在SecurityConfig中使用函数式编程来配置SecurityFilterChain,使用的方法和类和之前基本一致,只是成了函数式编程的方式而已。
/**
 * SpringSecurity相关配置,仅用于配置SecurityFilterChain
 * Created by macro on 2019/11/5.
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private IgnoreUrlsConfig ignoreUrlsConfig;
    @Autowired
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Autowired(required = false)
    private DynamicAuthorizationManager dynamicAuthorizationManager;

    @Bean
    SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.authorizeHttpRequests(registry -> {
            //不需要保护的资源路径允许访问
            for (String url : ignoreUrlsConfig.getUrls()) {
                registry.requestMatchers(url).permitAll();
            }
            //允许跨域请求的OPTIONS请求
            registry.requestMatchers(HttpMethod.OPTIONS).permitAll();
            //任何请求需要身份认证
        })
        //任何请求需要身份认证
        .authorizeHttpRequests(registry-> registry.anyRequest()
            //有动态权限配置时添加动态权限管理器
            .access(dynamicAuthorizationManager==null? AuthenticatedAuthorizationManager.authenticated():dynamicAuthorizationManager)
        )
        //关闭跨站请求防护
        .csrf(AbstractHttpConfigurer::disable)
        //修改Session生成策略为无状态会话
        .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        //自定义权限拒绝处理类
        .exceptionHandling(configurer -> configurer.accessDeniedHandler(restfulAccessDeniedHandler).authenticationEntryPoint(restAuthenticationEntryPoint))
        //自定义权限拦截器JWT过滤器
        .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        return httpSecurity.build();
    }

}

其他

  • 由于Java EE已经变更为Jakarta EE,包名以javax开头的需要改为jakarta,导包时需要注意;

  • Spring Boot 3.2 版本会有Parameter Name Retention(不会根据参数名称去寻找对应name的Bean实例)问题,添加Maven编译插件参数解决;
<build>
    <plugins>
        <!--解决SpringBoot 3.2 Parameter Name Retention 问题-->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <compilerArgs>
                    <arg>-parameters</arg>
                </compilerArgs>
            </configuration>
        </plugin>
    </plugins>
</build>
  • 或者可以通过在参数上添加@Qualifier指定name来解决,注意如果使用此种方式,Swagger API文档中的请求参数名称也会无法推断,所以还是使用上面的方法吧。
/**
 * @auther macrozheng
 * @description 消息队列相关配置
 * @date 2018/9/14
 * @github https://github.com/macrozheng
 */
@Configuration
public class RabbitMqConfig {

    /**
     * 订单消息实际消费队列所绑定的交换机
     */
    @Bean
    DirectExchange orderDirect() {
        return ExchangeBuilder
                .directExchange(QueueEnum.QUEUE_ORDER_CANCEL.getExchange())
                .durable(true)
                .build();
    }
    
    /**
     * 将订单队列绑定到交换机
     */
    @Bean
    Binding orderBinding(@Qualifier("orderDirect") DirectExchange orderDirect,
                         @Qualifier("orderQueue") Queue orderQueue){
        return BindingBuilder
                .bind(orderQueue)
                .to(orderDirect)
                .with(QueueEnum.QUEUE_ORDER_CANCEL.getRouteKey());
    }
}    

运行部署

Windows

由于Spring Boot 3最低要求是JDK17,我们在Windows下运行项目时需要配置好项目的JDK版本,其他操作和之前版本运行一样。

Linux

在打包应用的Docker镜像时,我们也需要配置项目使用openjdk:17,这里在项目根目录下的pom.xml中修改docker-maven-plugin插件配置即可。

由于镜像使用了openjdk:17,我们在打包镜像之前还许提前下载好openjdk的镜像,使用如下命令即可,其他操作和之前版本部署一样。

docker pull openjdk:17

总结

今天主要讲解了mall项目升级Spring Boot 3版本的一些注意点,这里总结下:

  • 项目中使用的框架版本升级到了最新主流版本;
  • 从SpringFox迁移到了SpringDoc;
  • 商品搜索功能实现采用了Spring Data ES的新用法;
  • Spring Security使用了新用法;
  • 项目运行部署时需要使用JDK 17版本。

项目源码地址

注意Spring Boot 3版本代码在dev-v3分支里。

https://github.com/macrozheng/mall

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

长难句打卡5.8

If it is trying to upset Google, which relies almost wholly on advertising, it has chosen an indirect method: there is no guarantee that DNT by default will become the norm. 如果它想激怒几乎全靠广告业务运营的谷歌公司的话&#xff0c;那么它选择了一个间接的方…

软件合规 安全可控 | 企业软件合规化管理方案

软件合规&#xff0c;安全可控&#xff0c;这是当下企业运营中不可或缺的两个关键词。随着信息技术的迅猛发展&#xff0c;企业对于软件的需求与日俱增&#xff0c;然而&#xff0c;如何确保软件的合规性和安全性&#xff0c;却成为了摆在企业面前的一大难题。Ping32企业软件合…

[嵌入式系统-69]:RT-Thread-组件:网络组件“组”,RT-Thread系统通向外部网络世界的入口

目录 RT-Thread 提供的网络世界入口 - 网络组件 1. 总概 2. AT 3. Lwip&#xff1a; 轻量级IP协议栈 4. W5500 5. Netdev 6. RT-Thread SAL&#xff08;Socket Abstraction Layer&#xff09;套接字和BSD套接字区别 RT-Thread SAL 套接字接口示例 BSD 套接字接口示例 …

通过 Java 操作 redis -- list 列表基本命令

目录 使用命令 lpush&#xff0c;lrange&#xff0c;rpush 使用命令 lpop 和 rpop 使用命令 blpop&#xff0c;brpop 使用命令 llen 关于 redis list 列表类型的相关命令推荐看Redis - list 列表 要想通过 Java 操作 redis&#xff0c;首先要连接上 redis 服务器&#xff…

深圳比创达电子|EMI一站式解决方案:提升企业电磁兼容性的路径

随着电子技术的快速发展&#xff0c;电磁干扰&#xff08;EMI&#xff09;问题日益凸显&#xff0c;对电子设备的正常运行和性能稳定造成了严重影响。为了有效应对这一挑战&#xff0c;EMI一站式解决方案应运而生&#xff0c;成为众多企业和个人解决EMI问题的首选方案。 一、E…

在下游市场需求带动下 轮胎模具市场规模逐渐扩大

在下游市场需求带动下 轮胎模具市场规模逐渐扩大 轮胎模具是通过硫化、成型等工序生产各种轮胎的一种工具。轮胎模具是生产轮胎的关键设备之一&#xff0c;其性能直接影响到轮胎的耐用性和安全性。根据花纹加工工艺不同&#xff0c;轮胎模具加工工艺可分为精密铸造工艺、数控雕…

手动实现简易版RPC(四)

手动实现简易版RPC(四) 往期内容 手动实现简易版RPC&#xff08;一&#xff09;&#xff1a;RPC简介及系统架构 手动实现简易版RPC&#xff08;二&#xff09;&#xff1a;简单RPC框架实现 手动实现简易版RPC(三)&#xff1a;mock数据生成 前言 接上几篇博客我们实现了最…

MT2049 运动会进行中

思路&#xff1a;使用a存前缀和。遇见女生-1&#xff0c;遇见男生1。之后遍历a的时候&#xff0c;如果a[i]a[j]&#xff0c;则说明ij这个区间里男女生数量是一样的。所以即求ij这个区间最大长为多少。可以用l[]r[]数组记录某个数第一次出现的位置和最后一次出现的位置。 例如样…

【代码随想录——字符串】

1. KMP算法 最长相等前后缀 1.1 如何计算前缀表 前缀&#xff1a;是包含首字母&#xff0c;不包含尾字母的所有子串后缀&#xff1a;是包含尾字母&#xff0c;不包含首字母的所有子串 求最长相等前后缀的长度 假设我们有一个模式串&#xff1a;aabaaf 模式最长相等前后缀最…

使用公有云主机部署ftp服务被动模式(centos操作系统)

文章目录 前言一、FTP服务搭建1.1 部署服务1.2 修改配置文件1.3 重启服务1.3 配置项解答 二、安全组设置访问规则2.1配置监听端口2.2 配置数据端口三、使用ftp登陆工具测试3.1 使用工具进行测试 总结 前言 使用公有云上的云主机搭建FTP服务器。 步骤思路&#xff1a; 1、云主机…

20240507 ubuntu20.04+ros noetic 跑通lioslam

任务&#xff1a;跑通lioslam 主要参考博客 IMU激光雷达融合使用LIO-SAM建图学习笔记——详细、长文、多图、全流程_ubuntu_AIDE回归线-GitCode 开源社区 (csdn.net) 1.不要用这一句 wget -O ~/Downloads/gtsam.zip https://github.com/borglab/gtsam/archive/4.0.0-alpha2…

web安全day03

MYSQL注入&#xff1a; SQL 注入的原理、危害及防御措施 SQL 注入的原理&#xff1a;原本的 SQL 语句在与用户可控的参数经过了如拼接、替换等字符串操作后&#xff0c;得到一个新的 SQL 语句并被数据库解析执行&#xff0c;从而达到非预期的效果。 SQL 注入的危害&#xff…

Java集合简介

单列集合 双列集合

MySQL数据库及数据表的创建

1.创建一个名叫 db_classes 的数据库&#xff1a; 创建一个叫 db_classes 的数据库MySQL命令&#xff1a; create database db_classes; 运行效果&#xff1a; 创建数据库后查看该数据库基本信息MySQL命令&#xff1a; show create database db_classes; 运行效果&#xff…

社交媒体数据恢复:多闪

社交软件多闪是一款深受用户喜爱的社交应用&#xff0c;用于与朋友、家人保持联系。有时&#xff0c;多闪软件的聊天记录可能会丢失或被删除&#xff0c;这时用户需要进行数据恢复。本文将详细介绍多闪软件聊天记录的恢复过程。 一、多闪软件聊天记录恢复方法 从手机备份中恢…

Vue 中 $nextTick 的作用是什么?

目录 一、NextTick是什么 为什么要有nexttick 二、使用场景 三、实现原理 一、NextTick是什么 官方对其的定义 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法&#xff0c;获取更新后的 DOM 什么意思呢&#xff1f; 我们可以理解成&#xff0c…

一路串联电机的绕制原理

这里要说明的一点是 对于一路串联的电机&#xff0c;无论是一把线圈还是两把线圈&#xff0c;出来的都是只有两个线头&#xff0c;可看做一个整体来对待&#xff01; 绕制具体原理 同心式线圈绕制 前面说的都是等距式的 线圈绕制&#xff0c;下面我们讲解一下同心式的绕制办法…

Android studio 打开Device Mirroring方便调试

巧合下发现一个很好用的工具&#xff0c;在平时调试真机的时候在每次run app后都要低头找找手机看看效果。但是&#xff0c;用了AS上的Device Mirroring&#xff0c;你会发现根本不需要再低头点手机&#xff0c;调试方便一万倍啊。 话不多说&#xff0c;上图。直接就可以在电脑…

ogv转mp4怎么转?只需3个步骤~

OGV&#xff08;Ogg Video&#xff09;是一种开源的视频文件格式&#xff0c;起源于对数字媒体领域的开放标准的需求。作为Ogg多媒体容器的一部分&#xff0c;OGV的设计初衷是提供一种自由、高质量的视频编码方案&#xff0c;以满足多样化的应用需求。 支持打开MP4文件格式的软…