springboot3+jdk17+MP整合最新版jersey详细案例,正真做到拿来即用

news2024/9/28 1:26:43
如题,springboot3.x + java17 + MP 整合最新jersey,各种请求类型(实战/详解) + 文件上传下载 + jersey资源注册 + 拦截器(JWT) + 跨域处理 + 全局异常 + Valid注解校验 等等 ,除非你必须整合security,否则或许吧,再加上redis,直接用吧

一、首先从请求资源说起!

1. jersey基础请求,定义资源,添加注解等等,,,

post,put请求遇到的坑,下面有标注

import com.xxx.config.api.AbstractResource;
import com.xxx.entity.UserEntity;
import com.xxx.service.UserService;
import com.xxx.util.jwt.PassToken;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.springframework.beans.factory.annotation.Autowired;

// 资源可访问路径, 注意这个前面的 “/” 别吃了
@Path("/user")
// 回参以json格式返回
@Produces(MediaType.APPLICATION_JSON)
public class UserResource extends AbstractResource {

    @Autowired
    private UserService userService;

    @GET
    @Path("/info")
    // 获取个人信息,登录者,id通过header中的token获取
    public Response mine() {
        return this.ok(userService.mine());
    }

    @PUT
    @Path("/edit")
    // 这里增加了 @Valid 校验,看看实体对象中哪些属性不要的删掉
    // 另外,使用@Valid 校验,由于这里是java17,注意不要导入了javax的包,否则无效
    public Response edit(@Valid UserEntity entity) {
        userService.edit(entity);
        return this.successEdit();
    }

    @POST
    @Path("/login")
    // 自定义@PassToken 注解,不需要token也能访问
    @PassToken(canPass = true)
    // form传参,且请求类型必须为 application/x-www-form-urlencoded
    public Response login(@FormParam("username") String username,
                          @FormParam("password") String password) {
        return this.ok(userService.login(username, password));
    }

    @PUT
    @Path("/logout")
    // 登出,清除token
    public Response logout() {
        userService.logout();
        return this.success();
    }

    @PUT
    @Path("/register")
    // 使用 BeanParam 注解,可以自动将请求参数绑定到实体类上,但是实体必须使用 @FormParam 注解修饰属性,否则会报错
    // 而且,请求类型必须是 application/x-www-form-urlencoded
    // 并且必须加上无参构造和所有有参构造......不太建议使用......
    // 不加 @BeanParam 注解,请求类型必须是 application/json,简单,推荐
//    public Response register(@Valid @BeanParam UserEntity entity) {
    public Response register(@Valid UserEntity entity) {
        userService.register(entity);
        return this.success();
    }

}


// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------

// ----------------------------------------------------------------------------

// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------


// 针对如上的 @BeanParam ,实体为
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.FormParam;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serial;
import java.io.Serializable;

/**
 * @TableName user_t
 */
@TableName(value = "user_t")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserEntity implements Serializable {
    /**
     * 用户id
     */
    @FormParam("id")
    @TableId(type = IdType.AUTO)
    private Integer id;

    /**
     * 用户名称
     */
    @FormParam("name")
    @NotNull(message = "用户名不能为空!")
    private String name;

    /**
     * 密码
     */
    @FormParam("password")
    @NotNull(message = "密码不能为空!")
    @TableField(select = false)
    private String password;

    /**
     * 电话
     */
    @FormParam("phone")
    @NotNull(message = "电话号码不能为空!")
    private String phone;

    @Serial
    @TableField(exist = false)
    private static final long serialVersionUID = 1L;

}
2. 文件上传,本地文件下载/预览实现
import com.xxx.config.api.AbstractResource;
import com.xxx.config.exception.GenExceptCode;
import com.xxx.config.exception.ServiceException;
import com.xxx.util.FileUtil;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jdk.jfr.Description;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;

import java.io.File;
import java.io.InputStream;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;

@Path("/file")
@Produces(MediaType.APPLICATION_JSON)
public class FileResource extends AbstractResource {

    @POST
    @Path("/upload")
    @Description("上传文件")
    @Consumes(MediaType.MULTIPART_FORM_DATA + ";charset=UTF-8")
    public Response upload(@FormDataParam("file") InputStream inputStream,
                           @FormDataParam("file") FormDataContentDisposition disposition) {
        // fileName utf8 处理上传文件名称
        // 先decode转码,在用char转码
        String fileName = disposition.getFileName();

        // 兼容处理 RFC 6266规范 上传 文件编码格式
        String fileNamePrefix = "UTF-8''";
        boolean charFlag = fileName.contains(fileNamePrefix);
        if (charFlag) {
            fileName = URLDecoder.decode(fileName.replace(fileNamePrefix, ""), StandardCharsets.UTF_8);
        } else {
            fileName = new String(fileName.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
        }

        // file full path
        String filePath = "D:/files/" + fileName;
        File tempFile = new File(filePath);

        // 判断是否村子
        if (tempFile.isFile() && tempFile.exists()) {
            throw new ServiceException(GenExceptCode.Request_Param.name(), "上传失败,该文件已存在!");
        }

        // save file
        File file = FileUtil.saveFile(inputStream, tempFile);

        // 文件大小, 不要用 disposition 获取文件大小
//        disposition.getSize(); // 这个获取不到文件大小,不知道为啥。。。
        file.length();

        return this.success();
    }

    @GET
    @Path("/{id}/download")
    @Description("下载文件")
    // 浏览器请求:http://localhost:8080/{fileId}/download
    public Response download(@PathParam("id") Integer id) {
        // 通过id在数据库中获取到文件的具体路径,如"D:/files/xxx.docx"
        String filePath = "";
        File file = new File(filePath);
        //如果文件不存在,提示404
        if (!file.exists()) {
            return this.err404();
        }
        // 直接将file对象给jersey处理就好,这里只是简单的封装了下
        return this.successDownload(file);
    }

    @GET
    @Path("/{id}/preview")
    @Description("预览查看文件")
    // 浏览器请求:http://localhost:8080/{fileId}/preview
    public Response preview(@PathParam("id") Integer id) {
        // 通过id在数据库中获取到文件的具体路径,如"D:/files/xxx.docx"
        String filePath = "";
        File file = new File(filePath);
        //如果文件不存在,提示404
        if (!file.exists()) {
            return this.err404();
        }
        // 直接将file对象给jersey处理就好,这里只是简单的封装了下
        return this.successPreview(file);
    }

}

二、 跨域处理

(----------后续要在JerseyConfig中注册,这里先上代码----------)

import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;

import java.io.IOException;

/**
 * 跨域处理 此方法需要在 Jersey 中注册 资源 后才能使用
 */
public class CorsFilter implements ContainerResponseFilter {
    public void filter(ContainerRequestContext request, ContainerResponseContext c) throws IOException {
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            //浏览器会先通过options请求来确认服务器是否可以正常访问,此时应放行
            c.setStatus(HttpServletResponse.SC_OK);
        }
        c.getHeaders().add("Access-Control-Allow-Origin", "*");

        c.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization");
        c.getHeaders().add("Access-Control-Allow-Credentials", "true");

        c.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");

        // CORS策略的缓存时间
        c.getHeaders().add("Access-Control-Max-Age", "1209600");

    }

}

三、去除APPLICATION_FORM_URLENCODED请求的警告信息,extends HiddenHttpMethodFilter,输出请求响应

(这个不需要注册jersey,直接使用配置)

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.core.MediaType;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jetty.http.HttpStatus;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.filter.HiddenHttpMethodFilter;

import java.io.IOException;
import java.time.Duration;
import java.time.LocalDateTime;

/**
 * 去除APPLICATION_FORM_URLENCODED请求的警告信息
 *
 * @version 1.0
 * @since 1.0
 */
@Slf4j
@Configuration
public class HttpMethodFilter extends HiddenHttpMethodFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse r,
                                    FilterChain fc) throws ServletException, IOException {
        LocalDateTime startTime = LocalDateTime.now();
        if ((RequestMethod.POST.name().equals(request.getMethod()) || RequestMethod.PUT.name().equals(request.getMethod()))
                && MediaType.APPLICATION_FORM_URLENCODED.equals(request.getContentType())) {
            //Skip this filter and call the next filter in the chain.
            fc.doFilter(request, r);
        } else {
            //Continue with processing this filter.
            super.doFilterInternal(request, r, fc);
        }
        LocalDateTime endTime = LocalDateTime.now();
        Duration duration = Duration.between(startTime, endTime);
        String info = request.getMethod()
                + ">>>>>>"
                + request.getRequestURI()
                + "======>请求耗时: " + duration.toMillis() + " 毫秒,返回状态为:" + r.getStatus();
        if ("favicon.ico".equalsIgnoreCase(request.getRequestURI())) {
            return;
        }
        if (HttpStatus.OK_200 == r.getStatus()) log.info(info);
        else log.error(info);
    }
}

四、自定义RequestFilter implements ContainerRequestFilter

(----------后续要在JerseyConfig中注册,这里先上代码----------)

import com.xxx.config.api.AuditAware;
import com.xxx.config.api.ReturnResult;
import com.xxx.config.exception.GenExceptCode;
import com.xxx.util.jwt.JwtUtil;
import com.xxx.util.jwt.PassToken;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.ResourceInfo;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jetty.http.HttpStatus;

import java.lang.reflect.Method;

/**
 * 自定义拦截器实现
 */
public class RequestFilter implements ContainerRequestFilter {

    @Context
    private ResourceInfo resourceInfo;

    @Override
    public void filter(ContainerRequestContext requestContext) {
        // 在这里进行拦截和处理操作
        // 可以获取请求的信息,进行身份验证、参数校验等操作
        // 如果需要终止请求并返回响应,可以使用requestContext.abortWith方法

		// 获取自定义注解,当存在,则放行
        Method method = resourceInfo.getResourceMethod();
        if (method != null && method.isAnnotationPresent(PassToken.class)) {
            PassToken aspect = method.getAnnotation(PassToken.class);
            if (aspect != null && aspect.canPass()) {
                return;
            }
        }

		// 获取token
        String headerToken = JwtUtil.getJwtFromHeader(requestContext);
        String urlToken = JwtUtil.getJwtFromUrl(requestContext);

        if (StringUtils.isEmpty(headerToken) && StringUtils.isEmpty(urlToken)) {
            requestContext.abortWith(Response.ok(new ReturnResult(GenExceptCode.Operation_Denial.name(), "操作被拒!"))
                    .status(HttpStatus.UNAUTHORIZED_401).type(MediaType.APPLICATION_JSON_TYPE).build());
        } else {
            if (StringUtils.isEmpty(headerToken)) {
                if (StringUtils.isEmpty(urlToken)) {
                    requestContext.abortWith(Response.ok(new ReturnResult(GenExceptCode.Operation_Denial.name(), "操作被拒!"))
                            .status(HttpStatus.UNAUTHORIZED_401).type(MediaType.APPLICATION_JSON_TYPE).build());
                } else {
                    AuditAware.setUserId(JwtUtil.getUserIdFromToken(urlToken));
                }
            } else {
                AuditAware.setUserId(JwtUtil.getUserIdFromToken(headerToken));
            }
        }
    }
}

五、jersey配置类

这里必须注意,registerClasses的使用方法,不然你就一个一个资源添加吧
更多案例 ——》》 Springboot集成jersey打包jar找不到class处理

import com.xxx.config.exception.support.DefaultExceptionMapperSupport;
import com.xxx.config.exception.support.JsonMapperExceptionSupport;
import com.xxx.config.exception.support.UncaughtExceptionMapperSupport;
import com.xxx.config.exception.support.ValidationExceptionMapperSupport;
import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJsonProvider;
import org.glassfish.jersey.logging.LoggingFeature;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.context.annotation.Configuration;

/**
 * Jersey配置类
 */
@Configuration
public class JerseyConfig extends ResourceConfig {

    public JerseyConfig() {
        // 开启日志
        register(LoggingFeature.class);
        // 注册文件上传
        register(MultiPartFeature.class);
        // 注册跨域处理
        register(CorsFilter.class);
        // 注册自定义拦截器实现
        register(RequestFilter.class);
        // 注册json序列化
        register(JacksonJsonProvider.class);
        // 注册异常类资源
        register(DefaultExceptionMapperSupport.class);
        register(JsonMapperExceptionSupport.class);
        register(UncaughtExceptionMapperSupport.class);
        register(ValidationExceptionMapperSupport.class);

        // 注册包扫描 这个方法在开发使用没问题,但是打包jar后,找不到 class 文件
       // packages("com.xxx.api");

        // 定义扫描包含接口资源包
          registerClasses(ClassUtil.findAllClasses("com.xxx.api"));
    }
}

六、mabatis-plus简单配置

(看看就行)

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
// mapper java文件 路径扫描
@MapperScan("com.xxx.mapper")
// 开启事务管理
@EnableTransactionManagement
public class MybatisConfig {

    /**
     * 注册mybatis-plus分页插件
     */
    @Bean
    MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

七、自定义注解

使用见–userResource,实际逻辑见—RequestFilter

import java.lang.annotation.*;

@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
    boolean canPass() default false;
}

八、全局异常,先上使用案例,具体详见代码

当然,你也可以用最优雅的断言方式,,,

...
...
    public String login(String username, String password) {
        if (username == null || password == null) {
            throw new ServiceException(GenExceptCode.Request_Param.name(), "账户或密码不能为空!");
        }
        UserEntity userEntity = this.getOneByUsername(username, password);
        if (userEntity == null) {
            throw new ServiceException(GenExceptCode.Request_Param.name(), "账户或密码错误!");
        }
        AuditAware.setUserId(userEntity.getId());
        // -1 永不过期
        return JwtUtil.generateToken(userEntity.getId(), -1);
        // TODO 生成token后保存到redis
    }

...
...

九、效果

第一个有token,第二个没有token的效果

十、详见代码

配置文件,pom文件,以及其它源码

♥ – – – – git代码 – – – – ♥

欢迎指正

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

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

相关文章

uniapp页面嵌套其他页面的实现

功能: 类似于一个drawer&#xff0c;当主页面加载的时候会一并加载url对应的组件&#xff0c;当点击后以drawer形式显示组件里面的内容&#xff0c;可动画。 <navigator url"/pages/my/components/personalMessage" slot"right"><view><di…

git push出错: src refspec dev/xxx does not match any

使用如下命令gitp push出错: git push origin 远端分支名 git push origin dev/xxxx error: src refspec dev/xxxx does not match any error: failed to push some refs to https://git.woa.com/..... 解决方案 1: git push origin 本地分支名:远端分支名 解决方案2&#…

零代码编程:用kimichat批量重命名txt文本文件

一个文件夹中有很多个txt文本文件&#xff0c;需要全部进行重命名。 可以在kimichat中输入提示词&#xff1a; 你是一个Python编程专家&#xff0c;要完成一个关于批量重命名txt文本文件的Python脚本&#xff0c;下面是具体步骤&#xff1a; D:\Best Seller Books 这个文件夹…

Aztec的客户端证明

1. 引言 隐私保护 zk-rollup 的证明生成与通用 zk-rollup 的证明生成有很大不同。原因是给定交易中存在特定数据&#xff08;由私有函数处理&#xff09;&#xff0c;我们希望保持完全私有。在本文中&#xff0c;我们探讨了用于证明私有函数正确执行的客户端证明生成&#xff…

模拟实现字符串库函数(一)

在C语言的标准库中提供了很多针对字符串的库函数&#xff0c;这篇文章我们会学习并模拟实现几个简单的库函数 求字符串长度函数strlen strlen函数我们在之前已经用过很多次了&#xff0c;同时也模拟实现过&#xff0c;但是都不是模仿标准库中的strlen来实现&#xff0c;首先我…

【现代C++】可变参数模板

现代C中的可变参数模板是C11引入的一个功能&#xff0c;允许模板接受可变数量的参数&#xff0c;使得模板编程更加灵活和强大。 1. 基本用法 可变参数模板允许您创建接受任意数量参数的函数或类模板。 template<typename... Args> void func(Args... args) {// 处理参…

【Frida】【Android】01_手把手教你环境搭建

▒ 目录 ▒ &#x1f6eb; 导读开发环境 1️⃣ 环境搭建安装Android模拟器安装Frida CLI安装Frida Server端口重定向&#xff1a;adb forward 2️⃣ 运行测试spwan模式attach模式直接加载脚本 &#x1f4d6; 参考资料 &#x1f6eb; 导读 开发环境 版本号描述文章日期2024-03…

鸿蒙Harmony应用开发—ArkTS-类型定义

说明&#xff1a; 本模块首批接口从API version 7开始支持&#xff0c;后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 Resource 资源引用类型&#xff0c;用于设置组件属性的值。 可以通过$r或者$rawfile创建Resource类型对象&#xff0c;不可以修改Res…

MySQL学习笔记------SQL(2)

ziduanSQL DML 全称为&#xff1a;Data Manipulation Language&#xff0c;用来对数据库中表的数据记录进行增删改操作 插入数据 添加数据&#xff08;INSERT&#xff09; 给指定字段添加数据&#xff1a;INSERT INTO 表名(字段名1&#xff0c;字段名2&#xff0c;......…

Gogs - 一款极易搭建的自助 Git 服务

Gogs - 一款极易搭建的自助 Git 服务 1. 使用文档References Gogs https://gogs.io/ https://github.com/gogs/gogs Gogs (/gɑgz/) 项目旨在打造一个以最简便的方式搭建简单、稳定和可扩展的自助 Git 服务。使用 Go 语言开发使得 Gogs 能够通过独立的二进制分发&#xff0c;并…

基于SSM的NEUQ宿舍管理系统的设计与实现

基于SSM的NEUQ宿舍管理系统的设计与实现 获取源码——》公主号&#xff1a;计算机专业毕设大全 获取源码——》公主号&#xff1a;计算机专业毕设大全

【计算机网络实践】Cisco Packet Tracer局域网组网(FTP服务器通过交换机连接客户端)

本文为应对计算机网络第一次实验所写的预习报告 一、实验准备 一台装有Cisco Packet Tracer的PC机&#xff0c;一个大学生大脑。 二、了解FTP和Cisco Packet Tracer 具体内容可在百度搜索&#xff0c;在物理机上用FileZilla Server实现ftp可参看我前面的文章。Cisco Packet Tr…

C#绘制面形图

创建windows窗体应用 ,从工具箱添加Button和Panel using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Fo…

再仔细品品Elasticsearch的向量检索

我在es一开始有向量检索&#xff0c;就开始关注这方面内容了。特别是在8.X之后的版本&#xff0c;更是如此。我也已经把它应用在亿级的生产环境中&#xff0c;用于多模态检索和语义检索&#xff0c;以及RAG相关。 也做过很多的优化&#xff1a;ES 8.x 向量检索性能测试 & 把…

Vue3 上手笔记

1. Vue3简介 2020年9月18日&#xff0c;Vue.js发布版3.0版本&#xff0c;代号&#xff1a;One Piece&#xff08;n 经历了&#xff1a;4800次提交、40个RFC、600次PR、300贡献者 官方发版地址&#xff1a;Release v3.0.0 One Piece vuejs/core 截止2023年10月&#xff0c;最…

网络行为管理系统招标模板

项目名称&#xff1a;网络行为管理系统招标 一、项目背景 随着信息技术的迅猛发展&#xff0c;网络安全和数据保护已成为企业和组织面临的关键挑战。为了确保网络环境的安全、合规&#xff0c;并实现对网络行为的有效管理和审计&#xff0c;我们特此启动网络行为管理系统的招…

Linux系统下——PS1、PS2、PS3、PS4变量详解

目录 前言 一、PS1变量 1.PS1变量详解 2.PS1变量可用参数 3.彩色提示符 二、PS2变量 三、PS3变量 1.不使用PS3变量 2.使用PS3变量 四、PS4变量 前言 在Linux系统中&#xff0c;PS1、PS2、PS3和PS4是特定的环境变量&#xff0c;它们各自在控制提示符和菜单提示信息…

【算法每日一练]

目录 今日知识点&#xff1a; 辗转相减法化下三角求行列式 组合数动态规划打表 约数个数等于质因数的次方1的乘积 求一个模数 将n个不同的球放入r个不同的盒子&#xff1a;f[i][j]f[i-1][j-1]f[i-1][j]*j 将n个不同的球放入r个相同的盒子&#xff1a;a[i][j]a[i-j][j]a[…

[AIGC] Redis基础命令集详细介绍

Redis是一个强大的开源的键-值存储系统&#xff0c;被广泛应用于各种应用程序中。在使用Redis时&#xff0c;我们需要掌握一些基本的Redis命令来操作存储在其上的数据。这篇文章将向你介绍一些基本的Redis命令&#xff0c;让你能够更好地使用和理解Redis。 文章目录 启动Redis…

手撕算法-删除有序数组中的重复项

描述 很简单&#xff0c;就是&#xff0c;遇到重复的&#xff0c;只留一个&#xff0c;保存在数组的左半边。如&#xff1a;[0,0,1,1,1,2,2,3,3,4]变为[0,1,2,3,4] 分析 使用双指针。slow指针代表没重复的数应该放置的位置&#xff0c;fast表示遍历的不重复数字的位置&…