用户上下文打通与Feign的调用

在前几篇中已经将微信,网关与鉴权微服务全部打通,这次我们进行用户上下文打通,与微服务之间的调用。

用户上下文打通:

首先先思考一下,当我们成功登录的时候,网关会获取到当前用户相关的信息,比如说用户名等等,现在我们需要在catalogue服务中获取到当前用户的信息,我们应该怎么做呢,以前的思路就是,在catalogue服务中再进行一次获取不就好了。理论上可以,但是当微服务多了,难道每个微服务都需要进行获取吗,那么网关的意义也就不大了。

所以我们的目的是在网关层,将拦截的信息处理好,分发给每个微服务,做统一的处理,那么就引出了我们今天实现的第一个目的,打通用户的上下文。

新增包与实现类:

我们在gateway服务中新增filter包,新增一个类为LoginFilter

 LoginFilter:

代码如下:

package com.yizhiliulianta.gateway.filter;

import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import com.alibaba.nacos.api.utils.StringUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * 登录拦截器
 */
@Component
@Slf4j
public class LoginFilter implements GlobalFilter {
    @Override
    @SneakyThrows
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpRequest.Builder mutate = request.mutate();

        String url = request.getURI().getPath();
        log.info("LoginFilter.filter.url:{}", url);

        if (url.equals("/user/doLogin")) {
            return chain.filter(exchange);
        }

        SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
        //根据token获取对应的用户id
        String loginId = (String) tokenInfo.getLoginId();
        if (StringUtils.isEmpty(loginId)){
            throw new Exception("未获取到用户信息");
        }
        mutate.header("loginId",loginId);

        return chain.filter(exchange.mutate().request(mutate.build()).build());
    }
}

这是一个网关的登录过滤器,该类实现了全局过滤,整体的目的就是将当前访问的用户的唯一id,放入到转发的请求头中,在后续的服务里,如果需要,直接在请求头中获取即可,下面我来解释一下。

首先实现了GlobalFilter全局过滤器接口,重写filter方法。

ServerWebExchange封装了HTTP请求和响应的上下文,所以我们可以通过getRequest()获取当前的请求,然后我们将该次请求mutate()一个可更改的副本,下面就是判断是否为登录,因为登录是没有用户信息的,我们直接放行。

下面首先获取本次登录的tokeninfo信息,然后获取里面的用户id,然后将其放入到请求副本的请求头中,最后放行。放行的内容:首先构建一个ServerWebExchange的副本,这里封装的请求为,我们刚刚写入用户id的请求request,.build()就是将副本构建。

通过这段代码我们可以完成,通过网关将请求重写,放入用户id,方便转发的微服务进行获取。

上下文对象的封装:

首先我们来改变一下依赖,对不起哈哈哈我的原因,欠考虑了,依赖关系还是有点一点瑕疵,我们需要将controller层的spring-boot-starter-web依赖,转移到common层,然后将以前的日志spring-boot-starter-log4j2依赖删除,否则会冲突。后面可能还会有问题,到时候我们再慢慢整理,我们将公共的依赖全部抽到common层中,这个以后再说。

我们来到catalogue服务中,在commen层中新建以下包和类:

我们需要封装一个登录的上下文对象,代码如下:

package com.yizhiliulianta.movie.common.context;

import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 登录上下文对象
 */
public class LoginContextHolder {

    private static final InheritableThreadLocal<Map<String,Object>> THREAD_LOCAL = new InheritableThreadLocal<>();

    public static void set(String key,Object val){
        Map<String, Object> map = getThreadLocalMap();
        map.put(key,val);
    }

    public static String getLoginId(){
        return (String) getThreadLocalMap().get("loginId");
    }

    public static Object get(String key){
        Map<String, Object> threadLocalMap = getThreadLocalMap();
        return threadLocalMap.get(key);
    }

    public static void remove(){
        THREAD_LOCAL.remove();
    }

    public static Map<String,Object> getThreadLocalMap(){
        Map<String, Object> map = THREAD_LOCAL.get();
        if (Objects.isNull(map)){
            map = new ConcurrentHashMap<>();
            THREAD_LOCAL.set(map);
        }
        return map;
    }

}

在看这段代码之前,需要大家知道ThreadLocal是什么。

我对ThreadLocal的理解是当前线程的局部变量,其他线程无法访问,InheritableThreadLocal是对ThreadLocal的扩展,允许子线程访问父线程的变量,大家感兴趣可以自行查阅,因为后面我们可能使用多线程,所以这里就提前使用了,目前用ThreadLocal也是可以的。

首先我们new一个InheritableThreadLocal,规定其存储的内容为map集合,方便我们后续通过对应键来获取值。

先来看getThreadLocalMap(),这是实现ThreadLocal里变量唯一不变的关键,首先我们通过ThreadLocal的get方法获取该变量存储的内容,如果返回为null证明为空,证明该变量还没有内容,这时候我们再去new一个map集合,并将这个map集合放入到ThreadLocal里。 

这里的ConcurrentHashMap也是map,但是它是线程安全的,看作map表就好

 剩下的方法就是对map集合的获取和放入了,大家应该都可以看懂了,我就不去详细说了,其中的getLoginId就是在map集合中,通过loginid来获取用户id的方法封装。remove就是将ThreadLocal进行删除,确保在每次任务处理完之后,清理线程上下文,防止数据污染。

拦截器:

下面在common层中新增以下包和类:

我们需要在catalogue服务中实现一个登录拦截器,代码如下:

package com.yizhiliulianta.movie.common.interceptor;

import com.yizhiliulianta.movie.common.context.LoginContextHolder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 登录拦截器
 */
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String loginId = request.getHeader("loginId");
        if (StringUtils.isNotBlank(loginId)) {
            LoginContextHolder.set("loginId", loginId);
        }
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        LoginContextHolder.remove();
    }
}

首先我们实现HandlerInterceptor接口,去拦截访问该服务的请求,因为我们在网关中已经将当前用户的id放入到了请求头中,所以我们在进入该服务之前,我们先获取该请求头的信息,从中获取loginid,然后将其放入到我们封装的上下文对象中,然后放行。

工具类:

我们再封装一个工具类,方便我们获取当前用户的id,结构和代码如下:

package com.yizhiliulianta.movie.common.util;

import com.yizhiliulianta.movie.common.context.LoginContextHolder;

/**
 * 用户登录util
 */
public class LoginUtil {

    public static String getLoginId(){
        return LoginContextHolder.getLoginId();
    }


}

mvc拦截器配置:

我们已经将拦截器等准备完毕,最后只需要将拦截器配置到mvc中就可以了,代码和结构如下:

package com.yizhiliulianta.movie.common.config;


import com.yizhiliulianta.movie.common.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

/**
 * mvc全局处理
 */
@Configuration
public class GlobalConfig extends WebMvcConfigurationSupport {

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**");
    }

}

重写addInterceptors方法,调用拦截器链,将我们的拦截器添加到里面,拦截所有路径

测试:

下面我们来测试一下,我们能不能在catalogue服务中获取到当前登录的用户id,我们在domain层新增一条日志,如下:

下面我们使用apipost来进行测试:

 还记得我们的流程吗,先微信获取验证码,然后登录,获取token,然后将token添加到请求头,去进行服务访问

通过网关结课访问catalogue服务,查询电影信息

成功了!我们通过日志可以看到,我们成功获取了当前登录的用户id

Feign的使用:

前言:

首先将catalogue服务的common层的东西,也就是用户上下文打通的包与类,也粘贴到auth服务里,偷懒了,我不再去展示了

在使用之前我先更改一下表结构,给用户表增加一列,为用户的用户名,username是用户id,作为唯一标识,nickname为用户名称

再添加两个方法,一个是修改用户的用户名,一个是查询用户信息

代码我不去细说了,简单的增删改查,大家知道实现了什么就好,看下aippost的效果:

下面我们开始改造我们现在的服务

我们明确一下我们的目标,我们需要在catalogue服务中,获取当前用户的详细信息,也就是说,我们需要在catalogue服务中调取auth服务的方法,这是微服务之间的调用,那么引入我们的Feign

一,依赖:

我们在api层引入需要的依赖,如下:

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>3.0.7</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
            <version>3.0.6</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>
    </dependencies>

 然后在auth服务的common层中将api引入:

        <dependency>
            <groupId>com.yizhiliulianta</groupId>
            <artifactId>movie-auth-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

二,重构结构:

我们将对应的枚举,和AuthUserDTO放入到api层中,结构如下

然后我们将controller层的导包修改一下,不然会报错,修改成api层的实体类。

三:提供者:

接着我们在api包内添加一个接口,代码如下:

package com.yizhiliulianta.auth.api;

import com.yizhiliulianta.auth.entity.AuthUserDTO;
import com.yizhiliulianta.auth.entity.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

@FeignClient("movie-auth")
public interface UserFeignService {

    @RequestMapping("/user/getUserInfo")
    Result<AuthUserDTO> getUserInfo(@RequestBody AuthUserDTO authUserDTO);

}

在使用feign之前我们需要知道什么rpc,rpc是远程过程调用。

主要作用是,实现调用远程方法就像调用本地方法一样的体验,所以我认为,feign是一个rpc框架。而微服务之间的调用,实际上就是服务与服务之间,通过网络链接,比如http来请求,获取其接口的内容,所以feign或rpc就是简化了这个过程。

下面来看一下feign是如何使用的,首先我们需要构建一个feignapi接口,该接口下的方法是暴露给其他服务使用的,所以auth是提供者。那么在feign中,提供者提供暴露的接口,消费者就是调用其接口的服务。

我们在api接口中为暴露的接口使用requestmapping进行映射,外部调用该方法,实际上就是向映射的地址发起了一次请求。

最后我们在接口上打上@FeignClient注解,里面为auth服务注册到nacos里的名称。

四,消费者:

下面我们在catalogue服务中的common层里,新建一个rpc的包,和调用auth服务的实现类,在实体类包中添加一个userInfo的实体类,结构如下:

首先我们进行一层防腐,比如说我们想获取的只是用户的名称,不需要用户的id等信息,我们在实体包中构建我们想要的用户类,代码如下:

package com.yizhiliulianta.movie.common.entity;

import lombok.Data;

@Data
public class UserInfo {
    
    private String nickName;

}

下面我们开始编写userRpc,代码如下:

package com.yizhiliulianta.movie.common.rpc;


import com.yizhiliulianta.auth.api.UserFeignService;
import com.yizhiliulianta.auth.entity.AuthUserDTO;

import com.yizhiliulianta.auth.entity.Result;
import com.yizhiliulianta.movie.common.entity.UserInfo;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Component
public class UserRpc {

    @Resource
    private UserFeignService userFeignService;

    public UserInfo getUserInfo(String userName) {
        AuthUserDTO authUserDTO = new AuthUserDTO();
        authUserDTO.setUserName(userName);
        Result<AuthUserDTO> result = userFeignService.getUserInfo(authUserDTO);
        UserInfo userInfo = new UserInfo();
        if (!result.getSuccess()) {
            return userInfo;
        }
        AuthUserDTO data = result.getData();
        userInfo.setNickName(data.getNickName());
        return userInfo;
    }
}

首先我们将UserFeignService注入,然后新建一个方法来获取用户信息,实际上就是根据用户的id去封装成一个auth服务需要的userdto,然后发起请求,会返回给我们一个用户信息结果,我们再对结果进行一次封装,将我们需要的userinfo返回。

最后我们需要在启动类上加上注解

@EnableFeignClients(basePackages = "com.yizhiliulianta")

通过该注解,启用 Feign 客户端的功能。basePackages 属性用于指定要扫描的包,Spring 会在这些包中查找使用了 @FeignClient 注解的接口

五:测试:

下面我们还是在catalogue服务的domain层中进行测试,代码如下:

package com.yizhiliulianta.movie.domain.service.impl;

import com.alibaba.fastjson.JSON;
import com.yizhiliulianta.movie.common.entity.UserInfo;
import com.yizhiliulianta.movie.common.rpc.UserRpc;
import com.yizhiliulianta.movie.common.util.LoginUtil;
import com.yizhiliulianta.movie.domain.bo.MovieBO;
import com.yizhiliulianta.movie.domain.convert.MovieBOConverter;
import com.yizhiliulianta.movie.domain.service.MovieDomainServcie;
import com.yizhiliulianta.movie.infra.entity.TMovie;
import com.yizhiliulianta.movie.infra.service.TMovieService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
@Slf4j
public class MovieDomainServiceImpl implements MovieDomainServcie{

    @Resource
    private TMovieService movieService;

    @Resource
    private UserRpc userRpc;

    @Override
    public MovieBO selectMovie(MovieBO movieBO) {
        TMovie tmovie = MovieBOConverter.INSTANCE.convertBoToMovie(movieBO);
        TMovie movie = movieService.queryById(tmovie.getId());

        String loginId = LoginUtil.getLoginId();
        UserInfo userInfo = userRpc.getUserInfo(loginId);
        log.info("用户为:{}",userInfo.getNickName());
        
        MovieBO bo = MovieBOConverter.INSTANCE.convertToMovieBO(movie);
        if (log.isInfoEnabled()) {
            log.info("MovieController.selectMovie.bo:{}",
                    JSON.toJSONString(bo));
        }
        return bo;
    }
}

没什么变化,就是将UserRpc注入,然后调用其方法,通过satoken框架获取登录的id,然后传入,获取其用户的名称

下面我们启动所有服务,打开apipost,进行测试,查看是否能成功调用:

我们去后台看一下日志:

调用成功!到这里我们的微服务的调用就结束了 

微服务的使用到这里就差不多了,后面应该是定时任务,消息队列,和多线程的一些使用

谢谢大家的观看!

地址:

gitee:movieCloud: 微服务的练习

movieCloud: 微服务的练习 (gitee.com)

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

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

相关文章

IEEE754、linear11、linear16浮点数应用原理

IEEE754、linear11、linear16浮点数应用原理 1 浮点数应用1.1 IEEE754 浮点数标准1.2 PMBUS浮点数格式 2 浮点数原理2.1 IEEE754 浮点数解析2.2 PMBUS浮点数解析 3 浮点数代码应用3.1 IEEE754 浮点数应用3.1.1 将浮点变量赋值&#xff0c;直接打印整型&#xff08;32位&#xf…

Python兴趣编程百例:手把手带你开发一个图片转字符图的小工具

在数字世界的无尽探索中&#xff0c;我们时常被那些看似平凡的技术所启发&#xff0c;它们如同星辰般点缀着我们的创意天空。今天&#xff0c;我突发奇想&#xff0c;想要用Python开发一个将图片转化为字符画的小工具。这不仅是一次技术的实践&#xff0c;更是一场艺术与科技的…

STM32学习笔记(三)--EXTI外部中断详解

&#xff08;1&#xff09;配置步骤1.配置RCC 打开外设时钟2.配置GPIO 选择端口输入模式3.配置AFIO 选择要用的一路GPIO 连接至EXTI 4.配置EXTI 选择边沿触发方式 上升沿 下降沿 双边沿 选择触发响应方式 中断响应 事件响应 5.配置NVIC 选择一个合适的优先…

乡村养老服务管理系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;医疗人员管理&#xff0c;乡村志愿者管理&#xff0c;文娱活动管理&#xff0c;活动报名管理&#xff0c;医疗保健管理 前台账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;文娱活…

运维技术栈总结

文章目录 Linux CommandBasecd/lschmod/chown/chgrpvi/vimscptarsudf Installrpmyumdeb/apt Filtertailgrepawkfindnetstatechotelnetwhereistouch/mkdirgzip/rar/tar Statistics Linux MonitorCPUtophtopsar Memoryfreevmstat I/Oiostatpidstatiotop Networknetstatiftoptcpdu…

Azure创建虚拟机

Azure创建虚拟机 一、创建步骤(1)登录到Azure portal(2)启动新实例(3)填写必要信息选择系统镜像(4)选择实例类型(5)配置管理员帐户和入站端口规则(6) 磁盘:保持默认(7) 网络:保持默认(8) 管理:保持默认(9) Monitoring:Boot diagnostics选择Disable(10) 最后直接点击查看 + …

屏蔽房是做什么用的?为什么需要定期检测?

屏蔽房对于不了解的人来说&#xff0c;可能光看名字不知道是做什么的&#xff0c;但是对于一些企业或者机构&#xff0c;却是再熟悉不过的了。和名字一样&#xff0c;屏蔽房是对空间内的信号以及一些外界环境条件进行隔绝&#xff0c;在一些有特殊要求的企业机构中&#xff0c;…

STM32中五个时钟源:HSI、HSE、LSI、LSE、PLL

时钟系统是处理器的核心&#xff0c;或者说时钟是单片机的心脏。 1.单片机内部需要储存器、累加器&#xff0c;这些都需要逻辑门电路。比如锁存器就是一个D触发器&#xff0c;而触发器的置1、清0、置数的功能都需要跳变沿。D触发器就是上升沿后存入数据&#xff0c;而这个上升…

硬件电路基础【5.二极管】

二极管 前言一、基本原理1.1 介绍1.2 结构组成1.3 符号1.4 正负极判断 二、特性参数开关电路注意的参数极限特性电气特性特性曲线 三、应用场景稳压二极管原理故障特点连接方式参数最大额定参数电气特性特性曲线 应用典型的串联型稳压电路过压保护稳压二极管的应用与选择 肖特基…

如何使用ChatGPT等大模型翻译视频?2024最新翻译技巧分享

随着全球化的浪潮&#xff0c;跨语言沟通的需求日益增长。视频&#xff0c;作为一种生动直观的表达方式&#xff0c;也越来越需要跨越语言的障碍&#xff0c;触达更广泛的受众。因此&#xff0c;视频翻译成为了一个重要的领域&#xff0c;为不同语言背景的人们打开了理解彼此、…

ChatGPT的问题与回复的内容导出(Chorme)

我给出两种方式&#xff0c;第一种方式无使用要求&#xff0c;第二种方式必须安装Chorme 个人更推荐第二种方式 第一种方式&#xff1a;使用chatgpt自带的数据导出 缺点&#xff1a;会将当前未归档的所有聊天记录导出&#xff0c;发送到你的电子邮箱中 第二种方式&#xff1a…

基于STM32的智能水产养殖系统(三)

智能水产养殖系统设计 背景 智能水产养殖系统的设计背景主要源于对传统养殖方式的现代化需求和技术进步的推动。以下是该背景的详细阐述&#xff1a; 现代化养殖需求增加&#xff1a; 随着人口增长和食品需求的提升&#xff0c;传统的水产养殖方式面临诸多挑战&#xff0c;如资…

基于51单片机的脉搏测量仪—心率计

基于51单片机的脉搏测量仪 &#xff08;仿真&#xff0b;程序&#xff0b;原理图&#xff0b;设计报告&#xff09; 功能介绍 具体功能&#xff1a; 本系统由STC89C51/52单片机LCD1602显示模块5mm红外接收管LM358运放电路按键模块等构成 1.手指放到红外对管中&#xff0c;2…

带你走进CCS光源——环形低角度光源LDR2-LA系列

机器视觉系统中&#xff0c;光源起着重要作用&#xff0c;不同类型的光源应用也不同&#xff0c;选择合适的光源成像效果非常明显。今天我们一起来看看CCS光源——工业用环形低角度光源LDR2-LA系列。 LDR2-LA系列 采用柔性基板&#xff0c;创造最佳倾斜角度。 通过从低角度向…

微信小程序 - 出于性能原因,对长行跳过令牌化。长行的长度可通过 “editor.maxTokenizationLineLength” 进行配置

问题描述 出于性能原因&#xff0c;对长行跳过令牌化。长行的长度可通过 “editor.maxTokenizationLineLength” 进行配置。 解决方案 设置 - 编辑器设置 - 更多编辑器设置... 搜索&#xff1a;maxtoken&#xff0c;原来是 20000&#xff0c;我改成了 200000 即可~

海南云亿商务咨询有限公司抖店开店怎么样?

在数字化浪潮席卷全球的今天&#xff0c;电商行业日新月异&#xff0c;其中抖音电商以其独特的短视频直播模式&#xff0c;迅速崛起成为电商领域的新贵。海南云亿商务咨询有限公司&#xff0c;作为抖音电商服务的佼佼者&#xff0c;凭借专业的团队和丰富的经验&#xff0c;致力…

批量导出兜底回复对话,迭代优化聊天机器人 | Chatopera 云服务

持续优化知识库 聊天机器人的知识库&#xff0c;对话技能&#xff0c;需要长期的优化。这是因为&#xff0c;一方面&#xff0c;初期上线的机器人所依赖的数据量通常有限&#xff1b;另一方面&#xff0c;市场不断变化&#xff0c;客户产品新的问题。 上线聊天机器人的目的之…

基于Django、Bootstrap的电影推荐系统,算法基于用户的协同过滤算法,有爬虫有可视化后台

背景 基于Django和Bootstrap的电影推荐系统结合了用户协同过滤算法&#xff0c;通过爬虫技术获取电影数据&#xff0c;并在可视化后台展示推荐结果。该系统旨在提供个性化的电影推荐服务&#xff0c;帮助用户发现符合其喜好的电影。 用户协同过滤算法是一种常用的推荐算法&am…

蓝卓创始人褚健:工厂操作系统+APP,加速工业数字化转型

如何让众多的中小企业通过低成本的方式实现收益&#xff0c;享受到工业互联网、数字化转型带来的效益&#xff0c;是解决中小企业数字化转型难的核心问题。 中小企业规模庞大&#xff0c;数字化转型压力巨大 褚健表示&#xff0c;中国拥有最庞大的工业企业集群&#xff0c;全国…

STM32高级控制定时器(STM32F103):PWM输出模式

目录 概述 1 PWM模式介绍 2 PWM类型 2.1 PWM边缘对齐模式 2.2 PWM中心对齐模式 3 使用STM32Cube配置PWM 3.1 STM32Cube配置参数 3.2 生成Project 4 设置PWM占空比 4.1 函数介绍 4.3 函数源码 5 测试代码 5.1 编写测试代码 5.2 函数源码 6 运行代码 概述 本文主…