随手记录第十话 -- 升级SpringBoot3.0 + JDK17的踩坑记录

news2024/11/20 20:43:03

随着有些jar包的升级,JDK1.8已经不是最稳定的版本了。

前段时间接触到Web3相关,jar包的编译最低要JDK13了,碰巧另一个使用Kotlin写的jar包依赖需要17了,那就直接上17吧,同时Springboot也上到3.0。

1. 框架说明

Springboot3.0 + SpringSecurity + Swagger,数据库Jpa和mybatis都可,缓存使用redis。
先看主pom.xml依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.0.0</version>
    <relativePath/>
</parent>

依赖项

<!--Spring boot 安全框架-->
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-security</artifactId>
 </dependency>
<!-- Swagger UI 相关 2.0.4版本存在问题 -->
 <dependency>
     <groupId>org.springdoc</groupId>
     <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
     <version>2.2.0</version>
 </dependency>
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-validation</artifactId>
 </dependency>

服务之间调用采用openfeign,<openfeign.version>12.2</openfeign.version>

<!--Spring boot openfeign-->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-core</artifactId>
    <version>${openfeign.version}</version>
</dependency>
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
    <version>${openfeign.version}</version>
</dependency>
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-jackson</artifactId>
    <version>${openfeign.version}</version>
</dependency>
<!--spring契约-->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-spring4</artifactId>
    <version>${openfeign.version}</version>
</dependency>

至于其他的依赖JWT,Redis,Jpa,Mybatis就不贴了

2. SpringSecurity配置

和2.x版本的变化的还是挺大的,3.0采用的是是流式写法。

2.1 验证规则

public class SpringSecurityConf {


    @Autowired
    ApplicationContext applicationContext;

    @Bean
    public PasswordEncoder passwordEncoder() {
        // 密码加密方式
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public JwtTokenOncePerRequestFilter authenticationJwtTokenFilter() {
        return new JwtTokenOncePerRequestFilter();
    }


    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // 获取匿名标记
        Map<String, Set<String>> anonymousUrls = this.getAnonymousUrl();
        http
                //禁言basic明文
                .httpBasic().disable()
                //前后端分离不用卡跨域
                .csrf().disable()
                //禁用默认登录页
                .formLogin().disable()
                //授权异常
                .exceptionHandling(exceptions -> {
                    //403处理类 无法访问  401处理类 身份信息验证错误
                    exceptions.authenticationEntryPoint(getAuthenticationEntryPoint()).accessDeniedHandler(getAccessDeniedHandler());
                })
                //http请求
                .authorizeHttpRequests(registry -> {
                    registry.requestMatchers(HttpMethod.GET, anonymousUrls.get(RequestMethodEnum.GET.getType()).toArray(new String[0])).permitAll()
                            .requestMatchers(HttpMethod.POST, anonymousUrls.get(RequestMethodEnum.POST.getType()).toArray(new String[0])).permitAll()
                            .requestMatchers(HttpMethod.DELETE, anonymousUrls.get(RequestMethodEnum.DELETE.getType()).toArray(new String[0])).permitAll()
                            .requestMatchers(HttpMethod.PUT, anonymousUrls.get(RequestMethodEnum.PUT.getType()).toArray(new String[0])).permitAll()
                            .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                    ;
                    //添加白名单的
                    this.addWhiteListAuthorize(registry);
                    //添加自定义的
                    this.addCustomAuthorize(registry);
                    //默认全部放行
//                    registry.requestMatchers("/**").permitAll();
                    // 所有请求都需要认证
                    registry.anyRequest().authenticated();
                })
                //验证service类
//                .authenticationProvider()
                .addFilterBefore(this.getDefaultFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    //可重写该
    public void addWhiteListAuthorize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
        String[] PASS_PATH_GROUP = new String[]{
                "/*.html",
                "/webSocket/**",
                "/swagger-ui/*",
                "/*/api-docs/*",
                "/*/api-docs*",
                "/actuator/*",
                "/error",
        };
        for (String str : PASS_PATH_GROUP) {
            registry.requestMatchers(str).permitAll();
        }
    }

    //例如system有不同的处理 可重写该方法
    public void addCustomAuthorize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
        //hasRole 需要在权限面前加上ROLE_ hasAuthority/则不需要
        registry.requestMatchers("/api/**").hasAuthority(SecurityConstant.AUTH_USER)
                .requestMatchers("/admin/**").hasAuthority(SecurityConstant.AUTH_MANAGER);
    }


    public AccessDeniedHandler getAccessDeniedHandler() {
        return (request, response, e) -> {
            log.error("403 Forbidden,URI:{}", request.getRequestURI());
            //当用户在没有授权的情况下访问受保护的REST资源时,将调用此方法发送403 Forbidden响应
            response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());
        };
    }

    public AuthenticationEntryPoint getAuthenticationEntryPoint() {
        return (request, response, e) -> {
            log.error("401 无凭据,{},URI:{}", request.getMethod(), request.getRequestURI());
            // 当用户尝试访问安全的REST资源而不提供任何凭据时,将调用此方法发送401 响应
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e == null ? "Unauthorized" : e.getMessage());
        };
    }


    //默认的jwt校验器
    public JwtTokenOncePerRequestFilter getDefaultFilter() throws Exception {
        return new JwtTokenOncePerRequestFilter();
    }

	//自定义授权开放的注解 例如AnonymousGetMapping 注解了的方法放开权限
    public Map<String, Set<String>> getAnonymousUrl() {
        // 搜寻匿名标记 url: @AnonymousAccess
        RequestMappingHandlerMapping requestMappingHandlerMapping =
                (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping");
        Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
        // 获取匿名标记
        Map<String, Set<String>> anonymousUrls = new HashMap<>(6);
        Set<String> get = new HashSet<>();
        Set<String> post = new HashSet<>();
        Set<String> put = new HashSet<>();
        Set<String> patch = new HashSet<>();
        Set<String> delete = new HashSet<>();
        Set<String> all = new HashSet<>();
        for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) {
            HandlerMethod handlerMethod = infoEntry.getValue();
            AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class);
            if (null != anonymousAccess) {
                List<RequestMethod> requestMethods = new ArrayList<>(infoEntry.getKey().getMethodsCondition().getMethods());
                RequestMethodEnum request = RequestMethodEnum.find(requestMethods.size() == 0 ? RequestMethodEnum.ALL.getType() : requestMethods.get(0).name());
                Set<PathPattern> pathPatterns = infoEntry.getKey().getPathPatternsCondition().getPatterns();
                for (PathPattern pattern : pathPatterns) {
                    switch (Objects.requireNonNull(request)) {
                        case GET:
                            get.add(pattern.getPatternString());
                            break;
                        case POST:
                            post.add(pattern.getPatternString());
                            break;
                        case PUT:
                            put.add(pattern.getPatternString());
                            break;
                        case PATCH:
                            patch.add(pattern.getPatternString());
                            break;
                        case DELETE:
                            delete.add(pattern.getPatternString());
                            break;
                        default:
                            all.add(pattern.getPatternString());
                            break;
                    }
                }
            }
        }
        anonymousUrls.put(RequestMethodEnum.GET.getType(), get);
        anonymousUrls.put(RequestMethodEnum.POST.getType(), post);
        anonymousUrls.put(RequestMethodEnum.PUT.getType(), put);
        anonymousUrls.put(RequestMethodEnum.PATCH.getType(), patch);
        anonymousUrls.put(RequestMethodEnum.DELETE.getType(), delete);
        anonymousUrls.put(RequestMethodEnum.ALL.getType(), all);
        return anonymousUrls;
    }
}

2.2 拦截器

每个请求在满足权限之后都会走的入口类

public class JwtTokenOncePerRequestFilter extends OncePerRequestFilter {

    RedisConf redisConf;

    public JwtTokenOncePerRequestFilter() {
    }
	//如果有自定义拦截请求处理 
	//则重写验证规则中的getDefaultFilter方法 传入自定义拦截器
    public JwtTokenOncePerRequestFilter(RedisConf redisConf) {
        this.redisConf = redisConf;
    }


    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
                                    FilterChain chain) throws ServletException, IOException {
        String token = JwtUtils.getToken(req);
        if (token == null) {
            //可能会存在前端session缓存问题
            SecurityContextHolder.getContext().setAuthentication(null);
            chain.doFilter(req, res);
            return;
        }
        try {
            //验证token 并取出对应信息
            Authentication authentication = JwtUtils.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);

            //子类自定义操作 例如 存储,redis过期/续期
            this.handler(token, authentication);

            chain.doFilter(req, res);
        } catch (Exception e) {
            GlobalExceptionEnum ex = GlobalExceptionEnum.TOKEN_IS_ERROR;
            logger.error(e.getMessage());
            res.setContentType("application/json; charset=utf-8");
            res.setStatus(HttpStatus.UNAUTHORIZED.value());
            res.getWriter().write("{\"code\": " + HttpStatus.UNAUTHORIZED.value() + ", \"message\": \"" + ex.getText() + "\"}");
            res.getWriter().close();
        }
    }

    public void handler(String token, Authentication authentication) {
        if (redisConf != null && JwtUtils.containsRole(SecurityConstant.AUTH_USER)) {
            //仅针对用户服务做拦截 减少redis访问 对于 Token 为空的不需要去查Redis
            if (StringUtils.isNotEmptyObj(token)) {
                Long uid = JwtUtils.getLoginUid();
                String oldToken = redisConf.get(String.format(RedisKey.MEMBER_UID_TOKEN, uid));
                if (oldToken != null && !token.equals(oldToken)) {
                    logger.error("token不一致:token:" + token + ",oldToken:" + oldToken);
                    throw new BadRequestException(GlobalExceptionEnum.TOKEN_IS_ERROR.getText());
                }
            }
        }
    }
}

2.3 验证器

从验证规则来看,上面的验证器我是注释掉了的,因为登录注册是由自己实现,不是走的SpringSecurity的密码验证。这里也贴一下代码

//用账户密码登录的 会传入的明文密码 根据验证规则里面设置的加密算法进行加密
UsernamePasswordAuthenticationToken authenticationToken =
        new UsernamePasswordAuthenticationToken(authUser.getUsername(), password);

经过框架内部加密密码后,会回调到UserDetailsService#loadUserByUsername方法,该方法需要自己实现

@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
    private final UserService userService;
    private final RoleService roleService;
    private final DataService dataService;
    private final LoginProperties loginProperties;

    @Override
    public JwtUserDto loadUserByUsername(String username) {
        boolean searchDb = true;
        JwtUserDto jwtUserDto = null;
        if (searchDb) {
            UserDto user;
            try {
                user = userService.findByName(username);
            } catch (EntityNotFoundException e) {
                // SpringSecurity会自动转换UsernameNotFoundException为BadCredentialsException
                throw new UsernameNotFoundException("", e);
            }
            if (user == null) {
                throw new UsernameNotFoundException("");
            } else {
                if (!user.getEnabled()) {
                    throw new BadRequestException("账号未激活!");
                }
                jwtUserDto = new JwtUserDto(
                        user,
                        dataService.getDeptIds(user),
                        roleService.mapToGrantedAuthorities(user)
                );
                userDtoCache.put(username, jwtUserDto);
            }
        }
        return jwtUserDto;
    }
}

在这里面验证状态等,同时补齐权限,然后返回对应的dto,框架会根据dto里面的password来校验密码是否正确。

3. Swagger变化

在Springboot3中,原来的swagger-ui包已经不支持了,需要更改为springdoc

3.1 依赖

在最开始使用的2.0.4版本中,会出现@RequestParam(required = false)在swagger界面中不生效的问题。

<!-- Swagger UI 相关 2.0.4版本存在问题 -->
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.2.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

3.2 配置

和2版本相比,如果想添加Authorization,需要在类上添加如下注解

@Configuration
@SecurityScheme(name = "Authorization", type = SecuritySchemeType.HTTP, scheme = "bearer", in = SecuritySchemeIn.HEADER)
public abstract class SwaggerConf {


    @Bean
    public OpenAPI createOpenApi() {
        return new OpenAPI()
                .info(apiInfo())
                //设置授权信息
                .security(List.of(new SecurityRequirement().addList("Authorization")))
                //引入其他文档
//                .externalDocs(new ExternalDocumentation().description("百度一下")
//                        .url("http://www.baido.com"))
                ;
    }


    public Info apiInfo() {
        return new Info()
                .title("一款 web3.0 业务接口文档")
                .description("web3业务处理")
                .version("1.0");
    }


    //分组
    @Bean
    public GroupedOpenApi publicApi() {
        return GroupedOpenApi.builder()
                .group("api")
                .pathsToMatch("/api/**")
                .build();
    }

    @Bean
    public GroupedOpenApi adminApi() {
        return GroupedOpenApi.builder()
                .group("admin")
                .pathsToMatch("/admin/**")
                .build();
    }
}

其他的配置大同小异。

3.3 注解的使用

原来的版本的注解我就不贴了,先看类上和方式上的注解

@Tag(name = "APP:FTMO数据相关接口")
@RestController
@RequestMapping("/api/app")
public class CmsController {

	//get参数的注解自行查询
	@Operation(summary = "test测试", description = "test接口说明")
    @AnonymousGetMapping("/test")
    public UserInfo test(@RequestBody UserInfoVO vo) {
		
	}
}

在实体类上的注解

@Schema(title = "用户数据VO")
@Data
public class UserInfoVO extends UserInfo {
   
	@Schema(title = "页码查询")
    Integer page;
    @Schema(title = "条数")
    Integer pageSize;
}

需要注意的是在同一规范返回的实体类上不用注解

@NoArgsConstructor
@Data
//加了这个注解回导致无法显示泛型的实体类
//@Schema(name = "ApiResult", description = "REST接口标准返回值 View Model")
public class ApiResultVO<T> implements Serializable {

    /**
     * 返回码
     */
    @Schema(title ="REST接口返回码")
    private Integer code;

    /**
     * 返回描述
     */
    @Schema(title ="REST接口返回消息")
    private String message;

    /**
     * 返回数据
     */
     //这个DataVO类上不影响
    private DataVO<T, Object> content;
}

3.4 效果图

在这里插入图片描述
传入参数说明
在这里插入图片描述
返回参数实体类说明
在这里插入图片描述

4.其他

配置文件redis,移动多了一层data目录

  data:
    redis:
      database: 0
      #数据库索引
      host: 192.168.0.100
      port: 6379
      password:
      #连接超时时间
      timeout: 5000

过去有一段时间了,今天才抽时间记录一下,还有其他问题会继续更新。
以上就是本文的全部内容了!

上一篇:随手记录第九话 – Java框架整合篇
下一篇:随手记录第十一话 – xxx

非学无以广才,非志无以成学。

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

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

相关文章

哪个牌子的护眼灯防蓝光效果好?2023防蓝光护眼灯推荐

可以肯定的是&#xff0c;护眼灯一般可以达到护眼的效果。 看书和写字时&#xff0c;光线应适度&#xff0c;不宜过强或过暗&#xff0c;护眼灯光线较柔和&#xff0c;通常并不刺眼&#xff0c;眼球容易适应&#xff0c;可以防止光线过强或过暗导致的用眼疲劳。如果平时生活中需…

Python国庆祝福

系列文章 序号文章目录直达链接1浪漫520表白代码https://want595.blog.csdn.net/article/details/1306668812满屏表白代码https://want595.blog.csdn.net/article/details/1297945183跳动的爱心https://want595.blog.csdn.net/article/details/1295031234漂浮爱心https://want…

软件开发线上维护方案

编写软件维护方案是确保软件系统长期稳定运行和满足不断变化需求的关键步骤。以下是编写软件维护方案的一般步骤和建议&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 文档概述&#xff1a; 开始文档…

Java (day 3)方法、数组、面向对象和异常

Java方法、数组、面向对象和异常 1.方法1.1 什么是方法&#xff1f;1.2 方法的定义1.3 方法的调用1.4 值传递和引用传递1.5 方法的重载1.6 命令行传参1.7 可变参数1.8 递归 2.数组2.1 数组概述2.2 数组声明创建2.3 三种初始化及内存分析和总结&#xff08;1&#xff09;java内存…

前端本地开发中,代理配置是如何解决跨域的?

文章目录 跨域&#xff08;Cross-Origin&#xff09;开发代理原理先说一下三个概念那代理到底是如何解决跨域的&#xff1f; 补充参考视频 跨域&#xff08;Cross-Origin&#xff09; 这里再说一下跨域的概念吧。 在Web开发中&#xff0c;浏览器限制了从一个不同来源&#xff…

【C++】415.字符串相加

题目描述&#xff1a; 给定两个字符串形式的非负整数 num1 和num2 &#xff0c;计算它们的和并同样以字符串形式返回。 你不能使用任何內建的用于处理大整数的库&#xff08;比如 BigInteger&#xff09;&#xff0c;也不能直接将输入的字符串转换为整数形式。 示例1&#x…

XPS虽没流行,但还在使用!在Windows 10中打开XPS文件的最佳方法

当Windows Vista发布时&#xff0c;微软推出了XPS格式&#xff0c;这是PDF的替代品。XPS文件格式并不是什么新鲜事&#xff0c;但从未获得过多大的吸引力。 因此&#xff0c;XPS&#xff08;XML Paper Specification&#xff09;文件是微软对Adobe PDF文件的竞争对手。尽管XPS…

Kafka三种认证模式,Kafka 安全认证及权限控制详细配置与搭建

Kafka三种认证模式,Kafka 安全认证及权限控制详细配置与搭建。 Kafka三种认证模式 使用kerberos认证 bootstrap.servers=hadoop01.com:9092,hadoop02.com:9092,hadoop03.com:9092,hadoop04.com:9092 security.

分布式微服务技术栈-SpringCloud<Eureka,Ribbon,nacos>

微服务技术栈 一、微服务 介绍了解1 架构结构案例与 springboot 兼容关系拆分案例拆分服务拆分-服务远程调用 2 eureka注册中心Eureka-提供者与消费者Eureka-eureka原理分析Eureka-搭建eureka服务Eureka-服务注册Eureka-服务发现 3 Ribbon组件 负载均衡Ribbon-负载均衡原理Ribb…

深入探求:中国精益生产与管理实践的崭新视角

经过多方位的了解&#xff0c;比之制造行业上的精益管理上的表现情况&#xff0c;认为目前国内的精益生产精益管理实践仍处于自我认知的水平。目前很多的企业前进的步伐还是主要依据市场经济发展所衍生出来的较为先进的工具运用&#xff0c;其战略性的管理处于局部优化再而达到…

2.3 如何使用FlinkSQL读取写入到JDBC(MySQL)

1、JDBC SQL 连接器 FlinkSQL允许使用 JDBC连接器&#xff0c;向任意类型的关系型数据库读取或者写入数据 添加Maven依赖 <dependency><groupId>org.apache.flink</groupId><artifactId>flink-connector-jdbc</artifactId><version>3.1…

基于RuoYi-Flowable-Plus的若依ruoyi-nbcio支持自定义业务表单流程的集成方法与步骤(一)

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 由于大家最自定义业务表单的整个集成方法还不熟悉&#xff0c;下面大概介绍一下这个流程与方法。 1、首先…

顿号在键盘上怎么打?教你4个输入方法!

“朋友们&#xff0c;我正在准备一篇期末论文&#xff0c;但是文章里的顿号我一直输入不了。顿号在键盘上应该怎么输入呀&#xff1f;谁能教教我呢&#xff1f;非常感谢&#xff01;” 在使用电脑编辑文档时&#xff0c;我们可能经常需要输入顿号。但有些朋友还不知道顿号在键盘…

Java 关键字:synchronized详解

synchronized详解 基本使用源码解析常见面试题好书推荐 基本使用 Java中的synchronized关键字用于在多线程环境下确保数据同步。它可以用来修饰方法和代码块 当一个线程访问一个对象的synchronized方法或代码块时&#xff0c;其他线程将无法访问该对象的其他synchronized方法或…

ppt录屏怎么导出来?学会这个,让分享更容易

ppt已经成为了日常工作与学习中必不可少的工具&#xff0c;而ppt屏幕录制功能&#xff0c;可以方便用户将他人的演讲或视频中的内容记录下来&#xff0c;以便进一步学习与研究。录制ppt演示并将其导出为视频文件&#xff0c;可以帮助我们进行分享&#xff0c;但是很多人不知道p…

el-upload实现上传文件夹

背景&#xff1a;如图一所示&#xff0c;最下面有一个黄色上传文件按钮&#xff0c;为手动上传而且上传区域有上传文件和上传文件夹的区分 所以需要在点击了上传文件夹做特殊处理使得el-upload可以上传文件夹 一、template区域 <el-uploadclass"upload-file"dra…

Prometheus metrics数据抓取解析

Prometheus node的监控数据如链接展示&#xff0c;我们希望能更加方便的看到监控数据&#xff0c;shodan对Prometheus metrics 的数据做了格式化处理。172.96.3.215:9100/metricshttp://172.96.3.215:9100/metrics 本文我自己实现了一个命令行工具&#xff0c;可以输出类shodan…

STR时,android发生了什么(一)

在QA的基线中&#xff0c;触发android进入STR流程的方式是向qvm注入power key 按下松开的操作(对于单android的基线&#xff0c;我的理解方式应该也是相同的&#xff0c;都是模拟了power key的按下松开操作&#xff09;。 这个按键操作会通过virtio上报到VHAL层&#xff08;下…

用CSS+SVG做一个优雅的环形进度条

开门见山 先上最终效果图&#xff1a;&#xff08;Demo 传送门&#xff09; 其中进度、尺寸、环宽和颜色都可以非常方便地进行控制。 核心原理&#xff1a; 利用两个重叠的圆环形&#xff0c;通过对上层圆环弧长的控制来表示进度&#xff0c;下层圆环则作为辅助&#xff0c;…

59 分割等和子集

分割等和子集 NP 完全问题&#xff08;01背包&#xff09;题解1 二维DP题解2 空间优化DP&#xff08;改为1D&#xff09; 给你一个只包含正整数的非空数组 nums 。请你判断是否可以将这个数组分割成两个子集&#xff0c;使得两个子集的元素和相等。 示例 1&#xff1a; 输入&a…