2024年1月

声明

本文章中所有内容仅供学习交流,抓包内容、敏感网址、数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请联系我立即删除!
本文已在微信公众号发布

目标

网站

aHR0cHM6Ly9tb2JpbGUucGluZHVvZHVvLmNvbS8=

任务

获取商品列表接口中的内容

接口参数分析

我们获取的接口。可能在web端出不来。所以我们切换成手机端。

然后刷新网页。

然后我们跟栈一步一步去找这个anti_content

从第一个栈慢慢进就行了。

断点到这里 就能看到 anti_content的值了 但是这个值好像并不是生成值的地方。继续往上找。
走到异步方法里面。

然后找到这个方法。

发现这个方法只是个实例对象 没办法调用,但是我们因此知道了 这个方法。我们搜下这个方法试试

看到一共有五个。我们去最后这个方法里面看看。不要问为什么只看最后一个。

一步一步打断点 进入这个 n.getRiskControlInfoAsync()方法里面。
然后再一步一步打断点(这是个过程,需要很有耐心 一步一追踪)


最终发现anti_content的值。
其实这个时候不难发现。这是个ob混淆啊。

生成逻辑分析

我们先不着急去扣代码。或者是干别的。我们先来看看他的生成逻辑。
我们进入ne这个方法去看看里面的生成。

这个方法 前面都是一些逻辑层的移位与混淆还有定义变量。这里就不分析了。
这里我简单脱了一下混淆

function ne() {
    function n(e, t) {
        return B(t, e - 1064)
    }

    var r = t["CTxCC"]
        ,
        a = (e = [])["concat"].apply(e, [W[r](), P[r](), C[r](), E[r](), I[r](), D[r](), T[r](), N[r](), R[r](), A[r](), L[r](), M[r](), z[r]()].concat(function (e) {
            if (Array.isArray(e)) {
                for (var t = 0, n = Array(e.length); t < e.length; t++)
                    n[t] = e[t];
                return n
            }
            return Array.from(e)
        }(q[r]()), [F[r](), G[r](), Q[r](), H[r](), U[r](), V[r](), Y[r](), J[r](), X[r]()]));

    t["npRBP"](setTimeout, (function () {
            t["JSeyi"](ee)
        }
    ), 0);
    for (var c = a["length"]["toString"](2)["split"](""),
             s = 0;
         t["iSDtI"](c["length"], 16); s += 1)
        c["unshift"]("0");
    c = c["join"]("");
    var u = [];
    t["hNmVQ"](a["length"], 0) ? u["push"](0, 0) : t["xfDub"](a["length"], 0) && t["HvucD"](a["length"], t["kbnzE"](t["YrazO"](1, 8), 1)) ? u["push"](0, a["length"]) : t["fBcAN"](a['length'], t["kbnzE"](t["YrazO"](1, 8), 1)) && u["push"](d["parseInt"](c["substring"](0, 8), 2), d["parseInt"](c["substring"](8, 16), 2)),
        a = []["concat"]([1], [1, 0, 0], u, a);
    var l = o["deflate"](a)
        , p = []["map"]["call"](l, (function (e) {
            return String["fromCharCode"](e)
        }
    ));

    function h(e, t) {
        return B(t, e - 1797)
    }

    return t["dhItA"](t["yQQNR"], i["encode"](t["dhItA"](p["join"](""), f["join"]("")), i["budget"]))

}

其实可以看到

  1. r获取a的切片值。 通过合并多个数组(其实这里都是cookie和一些指纹。),从而让a获取q
    r
    展开后的内容再次合并形成一个新的数组。
  2. 之前获得的新数组重新展开生成一个二进制的新字符串。并且添加了补长度功能。
  3. 根据a 和 u判断从而继续生成新数组
  4. 再根据新数组 重新添加某些值。并且进行了压缩操作。
  5. 压缩并且再生成字符串 然后进行编码生成anti_content。

解决方案

其实这个有两种思路。根据刚刚我提供的思路
第一种:硬扣代码。
难度较大
第二种就是使用webpack去扣。其实也说不上来扣 全都抓取下来 然后补环境就行。

至于为什么是webpack。往上翻就行了。

首先这是一个一个模块。这就不多说了。
再次再往上滑。看到加载器.

然后把加载器和后面的列表全扣下来就行。
然后全部扣下来之后。我们不要放到代码中运行。
我们利用浏览器的这个片段。直接运行看看。

ok 也能直接出结果 感觉长度的话也像那么回事。现在我们搞到代码里面慢慢补就行了。

补环境


这里呢首先第一个错就是
Cannot use 'in' operator to search for 'ontouchstart' in undefined
这还是个移动端事件
手指触摸屏幕时触发,即使已经有手指在屏幕上也会触发。但是我们的代码中没有这个事件。所以就会报错。
这是个监听事件。我们先加上监听事件。看看然后会不会报错

document = {};
document.addEventListener = function addEventListener(type, listener, options, useCapture){};

然后发现就不报这个错了。

然后就开始报另一个错
根据网页API中所写
https://developer.mozilla.org/zh-CN/docs/Web/API/Screen/availWidth
这个方法是返回浏览器窗口可占用的水平宽度(单位:像素)。
但是这个是个方法 所以不能直接赋值 需要用方法的方式。

window.screen = Screen = function () {
    return {
        availHeight: 1032,
        availLeft: 0,
        availTop: 0,
        availWidth: 1920,
        colorDepth: 24,
        height: 1080,
        isExtended: false,
    }
}

这个补完之后还有个报错。

报错 Element 没有被定义。
这里呢,官方解释是这样的:

Element 是最通用的基类,Document 中的所有元素对象(即表示元素的对象)都继承自它。它只具有各种元素共有的方法和属性。更具体的类则继承自 Element。
这个方法要补的其实不少。但是我们可以简单定义一个对象。后期需要什么就补什么

Element = function(){}

剩下的其实也就是一样。缺什么补什么 可以挂个proxy框架。很简单。大家可以自己试试,

结果

结语

对了 总有人问我要代码。说实话很多人都是抱着白嫖的心思,问完就没有任何交集了。我感觉这样加好友说实话没啥意义。那与其这样,不如我挂个星球。大家想要代码 直接去里面拿。我也有收益,你也不会问的不清不楚。星球里遇到的问题我看见都会回答。

安装istio

$ curl -L https://istio.io/downloadIstio | ISTIO_VERSION=1.20.2 TARGET_ARCH=x86_64 sh -
$ cd istio-1.20.2
$ export PATH=$PWD/bin:$PATH

$ istioctl install --set profile=demo -y
✔ Istio core installed
✔ Istiod installed
✔ Egress gateways installed
✔ Ingress gateways installed
✔ Installation complete

卸载

istioctl x uninstall --purge

开启pod自动注入sidecar

$ kubectl label namespace default istio-injection=enabled --overwrite 

[root@elasticsearch02 istio]#  kubectl get namespace -L istio-injection
NAME                          STATUS   AGE     ISTIO-INJECTION
apisix                        Active   7d22h
bigdata                       Active   343d
cattle-impersonation-system   Active   674d
cattle-pipeline               Active   356d
cattle-prometheus             Active   568d
cattle-prometheus-p-whn4g     Active   568d
cattle-system                 Active   2y1d
default                       Active   2y1d    enabled

k8s和istio中的组件对比

查看istio的组件

查看pod

kubectl get pods -n istio-system
NAME                                   READY   STATUS    RESTARTS   AGE
istio-egressgateway-67b78cc94c-rm8t5   1/1     Running   0          7m41s
istio-ingressgateway-7d57fd4b-kn8nh    1/1     Running   0          7m41s
istiod-5ddd75f545-kc4lm                1/1     Running   0          8m36s

查看服务

kubectl get svc -n istio-system
NAME                   TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)                                                                      AGE
istio-egressgateway    ClusterIP      10.43.208.79    <none>        80/TCP,443/TCP                                                               4d17h
istio-ingressgateway   LoadBalancer   10.43.88.59     <pending>     15021:27218/TCP,80:25359/TCP,443:23532/TCP,31400:13324/TCP,15443:25647/TCP   4d18h
istiod                 ClusterIP      10.43.130.158   <none>        15010/TCP,15012/TCP,443/TCP,15014/TCP       

istio-ingressgateway
是 Istio 提供的一个组件,它作为 Kubernetes 集群的入口,接收从集群外部来的流量,并根据 Istio 的路由规则将流量转发到集群内部的服务。

在 Kubernetes 中,
istio-ingressgateway
通常被部署为一个
LoadBalancer
类型的 Service。如果你的 Kubernetes 集群运行在支持自动创建负载均衡器的云平台上(如 AWS、GCP、Azure 等),那么当你部署
istio-ingressgateway
时,云平台会自动为它创建一个外部的 L4 层负载均衡器,并将该负载均衡器的 IP 地址或者域名设置到 Service 的
status.loadBalancer.ingress
字段。

然而,如果你的 Kubernetes 集群运行在不支持自动创建负载均衡器的环境中(如裸金属服务器、某些私有云环境等),那么
istio-ingressgateway
的状态就会一直是
Pending
,因为 Kubernetes 无法为它创建外部的负载均衡器。这种情况下,你需要手动配置一个方式(如使用 NodePort、HostNetwork 或者使用外部的负载均衡器软件)来让外部流量能够访问到
istio-ingressgateway

总的来说,
istio-ingressgateway
的作用是接收并转发从集群外部来的流量。如果它的状态一直是
Pending
,那么可能是因为你的环境不支持自动创建负载均衡器,你需要手动配置一个访问方式。

安装后组件的说明

k8s pod,istio service
在Kubernetes集群中安装 Istio 后,会创建一个名为 istio-system 的命名空间,并在其中部署一些核心的 Istio 组件。这些组件包括:

  1. istio-egressgateway
    :Istio出口网关(Egress Gateway)负责管理服务对外部服务的访问,允许服务在网格之外访问外部服务。它可以配置和执行流量控制、安全策略、路由等功能,以便管理服务对外部服务的通信。

  2. istio-ingressgateway
    :Istio入口网关(Ingress Gateway)用于接收来自外部流量并将其引导到网格内部的服务。它充当了整个服务网格的入口点,可以执行流量路由、负载均衡、TLS终止等功能。

  3. istiod
    :istiod 是 Istio 服务网格的核心组件,负责实现服务发现、流量管理、安全性和遥测等功能。istiod 运行着Pilot、Mixer 和 Citadel 等子组件,它们共同协作来提供服务网格的各种功能。

在Kubernetes中使用 Istio,你可以通过以下方式进行操作:

  • 部署微服务应用
    :将你的微服务应用程序部署到Kubernetes集群中,并且通过Istio的流量管理功能实现服务之间的通信、负载均衡和故障恢复等。

  • 配置流量控制
    :利用Istio的流量管理功能,可以对服务之间的流量进行路由、限速、重试策略等灵活的控制。

  • 实现安全策略
    :通过Istio的安全功能,可以在服务之间实现强大的身份验证、安全通信、访问控制等。

  • 收集和展示遥测数据
    :Istio提供了丰富的遥测功能,可以帮助你收集并展示服务之间的流量、延迟、错误等数据,从而进行监控和故障排查。

总之,在Kubernetes中使用 Istio,你可以利用其丰富的功能来实现微服务架构中的流量管理、安全控制、遥测分析等需求,从而更好地管理和运维你的微服务应用程序。

实例体现

要配置 Istio 的流量管理功能,你需要使用 Istio 的资源对象(如 VirtualService、DestinationRule 等)来定义你的流量控制策略。下面是一些基本的步骤和示例,帮助你开始配置 Istio 的流量管理功能:

demo天气例子

apiVersion: v1
kind: Service
metadata:
  name: frontend
  labels:
    app: frontend
    service: frontend
spec:
  ports:
  - port: 3000
    name: http
  selector:
    app: frontend
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-v1
  labels:
    app: frontend
    version: v1 # 注意,这块对应的是istio配置中的DestinationRule里的label,DestinationRule根据v1找到pod中version为v1的pod
spec:
  selector:
    matchLabels:
      app: frontend
      version: v1
  replicas: 1
  template:
    metadata:
      labels:
        app: frontend
        version: v1
    spec:
      containers:
      - name: frontend
        image: istioweather/frontend:v1
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: advertisement
  labels:
    app: advertisement
    service: advertisement
spec:
  ports:
  - port: 3003
    name: http
  selector:
    app: advertisement
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: advertisement-v1
  labels:
    app: advertisement
    version: v1
spec:
  selector:
    matchLabels:
      app: advertisement
      version: v1
  replicas: 1
  template:
    metadata:
      labels:
        app: advertisement
        version: v1
    spec:
      containers:
      - name: advertisement
        image: istioweather/advertisement:v1
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 3003
---
apiVersion: v1
kind: Service
metadata:
  name: forecast
  labels:
    app: forecast
    service: forecast
spec:
  ports:
  - port: 3002
    name: http
  selector:
    app: forecast
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: forecast-v1
  labels:
    app: forecast
    version: v1
spec:
  selector:
    matchLabels:
      app: forecast
      version: v1
  replicas: 1
  template:
    metadata:
      labels:
        app: forecast
        version: v1
    spec:
      containers:
      - name: forecast
        image: istioweather/forecast:v1
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 3002
---

部署istio服务

步骤一:创建Gateway

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: weather-gateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway # use istio default controller
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*" #对所有域名都生效

步骤一:创建 VirtualService

VirtualService 定义了请求应该如何路由到目标服务。例如,你可以根据请求的路径、主机名等条件将请求路由到不同的服务版本。

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: my-service
spec:
  hosts:
  - "vue.xip.com" #这里配置哪个域名走istio流量控制
  http:
  - route:
    - destination:
        host: frontend
        port:
          number: 8080
        subset: v1

步骤二:创建 DestinationRule

DestinationRule 定义了对特定服务的流量策略,包括负载均衡、TLS设置等。

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: my-service
spec:
  host: frontend
  subsets:
  - name: v1
    labels:
      version: v1 # 转发到pod中标签有version的,并且值为v1的
  - name: v2
    labels:
      version: v2

查看已经创建的gateway,virtualService

kubectl get gateway -n <namespace>
kubectl describe gateway <gateway-name> -n <namespace>

kubectl get virtualservice -n <namespace>
kubectl describe virtualservice <virtualservice-name> -n <namespace>

查看部署后的gateway和virutalservice

gateway

[root@elasticsearch02 istio]# kubectl describe gateway weather-gateway  -n istio-system
Name:         weather-gateway
Namespace:    istio-system
Labels:       <none>
Annotations:  kubectl.kubernetes.io/last-applied-configuration:
                {"apiVersion":"networking.istio.io/v1alpha3","kind":"Gateway","metadata":{"annotations":{},"name":"weather-gateway","namespace":"istio-sys...
API Version:  networking.istio.io/v1beta1
Kind:         Gateway
Metadata:
  Creation Timestamp:  2024-01-24T03:08:34Z
  Generation:          4
  Managed Fields:
    API Version:  networking.istio.io/v1alpha3
    Fields Type:  FieldsV1
    fieldsV1:
      f:metadata:
        f:annotations:
          .:
          f:kubectl.kubernetes.io/last-applied-configuration:
      f:spec:
        .:
        f:selector:
          .:
          f:istio:
        f:servers:
    Manager:         kubectl
    Operation:       Update
    Time:            2024-01-24T03:08:34Z
  Resource Version:  190297478
  UID:               4e92bf14-dda0-468a-8c9f-00b3297b121e
Spec:
  Selector:
    Istio:  ingressgateway
  Servers:
    Hosts:
      *
    Port:
      Name:      http
      Number:    80
      Protocol:  HTTP
Events:          <none>

virtualservice

[root@elasticsearch02 istio]# kubectl describe virtualservice frontend-route -n ic-test
Name:         frontend-route
Namespace:    ic-test
Labels:       <none>
Annotations:  kubectl.kubernetes.io/last-applied-configuration:
                {"apiVersion":"networking.istio.io/v1alpha3","kind":"VirtualService","metadata":{"annotations":{},"name":"frontend-route","namespace":"ic-...
API Version:  networking.istio.io/v1beta1
Kind:         VirtualService
Metadata:
  Creation Timestamp:  2024-01-24T03:08:55Z
  Generation:          6
  Managed Fields:
    API Version:  networking.istio.io/v1alpha3
    Fields Type:  FieldsV1
    fieldsV1:
      f:metadata:
        f:annotations:
          .:
          f:kubectl.kubernetes.io/last-applied-configuration:
      f:spec:
        .:
        f:gateways:
        f:hosts:
        f:http:
    Manager:         kubectl
    Operation:       Update
    Time:            2024-01-24T03:08:55Z
  Resource Version:  190328684
  UID:               94c655d1-c48a-45b6-8f90-e0bf214702eb
Spec:
  Gateways:
    istio-system/weather-gateway
  Hosts:
    vue.xip.com
  Http:
    Match:
      Port:  80
    Route:
      Destination:
        Host:  frontend
        Port:
          Number:  3000
        Subset:    v1
Events:            <none>

在ingress中可视化体现,多个域名都通过istio ingress去管理入口流量

自动sidecar注入配置

要在 Istio 中开启自动的 sidecar 注入,你可以通过以下步骤来实现:

  1. 安装 Istio
    :首先需要安装 Istio 到你的 Kubernetes 集群中。你可以使用 istioctl 或者 Helm 来进行安装,具体安装步骤可以参考 Istio 官方文档。

  2. 开启自动 sidecar 注入
    :在安装 Istio 时,你可以选择开启自动 sidecar 注入功能。在 Istio 1.5 版本后,默认情况下自动 sidecar 注入功能是开启的,但在一些特定情况下可能需要手动确认是否已经开启。

  3. 确认命名空间标签
    :确保你要启用自动 sidecar 注入的命名空间已经添加了 Istio 的注入标签。你可以通过以下命令来查看:

    kubectl get namespace your-namespace -o=jsonpath='{.metadata.labels}'
    

    如果没有 istio-injection 标签,你可以使用以下命令来为命名空间添加标签:

    kubectl label namespace your-namespace istio-injection=enabled
    
  4. 部署新的 Pod
    :一旦确认自动 sidecar 注入功能已经开启,并且命名空间已经添加了 Istio 的注入标签,当你部署新的 Pod 时,Istio 将会自动为其注入 sidecar 容器。

通过以上步骤,你应该能够成功地开启 Istio 中的自动 sidecar 注入功能,并确保所有的服务都能够通过 Envoy 代理进行流量管理和控制。

istio中的对象汇总

在 Istio 中,ServiceEntry、DestinationRule 和 VirtualService 是用来定义和配置流量管理的重要概念。下面简要介绍它们的作用:

  1. ServiceEntry


    • 作用:ServiceEntry 允许你将外部服务引入到 Istio 网格中,或者定义对外部服务的访问策略。
    • 场景:当你需要让 Istio 管理对外部服务的访问时,可以使用 ServiceEntry 将外部服务引入到 Istio 网格中,并对其进行流量控制和安全策略的定义。
  2. DestinationRule


    • 作用:DestinationRule 定义了对特定服务的流量策略,包括负载均衡、TLS设置等。
    • 场景:在 Istio 中,你可以使用 DestinationRule 来定义对服务的流量策略,比如指定不同版本的服务之间的流量比例、超时设置、负载均衡策略等。
  3. VirtualService


    • 作用:VirtualService 定义了请求应该如何路由到目标服务,以及对请求的修改和转发。
    • 场景:通过 VirtualService,你可以定义基于请求的流量路由规则,比如根据请求的路径、主机名等条件将请求路由到不同的服务版本,还可以定义请求的修改和转发。

这些概念一起构成了 Istio 中强大的流量管理功能,使得你能够灵活地定义和控制服务之间的通信和流量行为。通过合理使用这些概念,你可以实现诸如流量控制、故障恢复、A/B 测试、蓝绿部署等高级的流量管理策略。

除了 ServiceEntry、DestinationRule 和 VirtualService,Istio 还有一些其他重要的对象和资源,用于定义和配置服务网格中的各种功能和策略。以下是一些常见的 Istio 对象:

  1. Gateway
    :用于定义 Istio 网格的入口点,允许流量进入网格并将其路由到适当的服务。

  2. Sidecar
    :在 Istio 中,每个部署的应用程序都会有一个称为 sidecar 的边车代理,它负责处理应用程序的所有网络通信,并与 Istio 控制平面进行交互。

  3. AuthorizationPolicy
    :用于定义对服务之间流量的访问控制策略,包括认证、授权和安全策略等。

  4. PeerAuthentication
    :用于定义服务之间的双向 TLS 认证策略,确保服务之间的通信是安全的。

  5. RequestAuthentication
    :用于定义对服务端点的请求认证策略,可以限制来自客户端的请求必须满足特定的认证条件。

  6. EnvoyFilter
    :允许你为 Envoy 代理定义自定义的过滤器和配置,以实现更灵活的流量控制和处理。

  7. ServiceRole

    ServiceRoleBinding
    :用于定义对服务的角色和权限控制,可以限制哪些服务具有对其他服务的访问权限。

这些对象一起构成了 Istio 的配置模型,通过对这些对象的定义和配置,你可以实现对服务网格中流量管理、安全性、可观察性等方面的精细控制。因此,熟悉并理解这些对象是使用 Istio 进行微服务架构管理的关键。

在istio中,Gateway,DestinationRule和VirtualService三者之间的关系可以用以下文字图示表示:

         +-------------------+
         |      Gateway      |
         +---------+---------+
                   |
                   v
         +---------+---------+
         |  VirtualService  |
         +---------+---------+
                   |
                   v
         +---------+---------+
         |  DestinationRule |
         +-------------------+

在这个示意图中,Gateway负责接收外部流量,并将其转发到内部服务。VirtualService定义了流量的路由规则,指定了如何将接收到的流量发送到不同的目标服务或版本。而DestinationRule定义了服务之间的通信策略,包括负载均衡、故障恢复等。因此,这三者之间存在一定的依赖关系,Gateway接收流量并将其传递给VirtualService,而VirtualService再根据定义的规则将流量分发给后端服务,同时DestinationRule定义了这些后端服务之间的通信策略。

Envoy Sidecar模式

当你将Istio部署到Kubernetes集群中时,它在外部流量转发的过程中起到了以下作用:

  1. 流量管理
    :Istio通过Envoy sidecar代理来管理流量,可以实现流量控制、负载均衡、故障恢复和A/B测试等功能。

  2. 安全
    :Istio提供了服务间的认证、加密通信和访问控制,确保了服务之间的安全通信。

  3. 监控
    :Istio收集并展示了整个服务网格的指标、日志和跟踪数据,帮助你更好地监控和调试服务。

  4. 策略执行
    :Istio可以通过定义网络策略来限制流量,例如路由规则、重试策略、超时设置等。

下面是一个简单的图示,描述了在Kubernetes集群中使用Istio的流量管理过程:

外部流量 -> Ingress -> Istio Ingress Gateway -> Envoy Sidecar -> 后端服务

在这个示意图中,外部流量首先经过Kubernetes的Ingress,然后被Istio Ingress Gateway接管,并通过Envoy sidecar代理进行流量管理,最终到达后端服务。

引言

中介者,何为中介者,顾名思义就是我们的在处理A和B之间的关系的时候,引入一个中间人,来处理这两者之间的关系,例如生活中我们需要去租房,买房,都会有中介,来处理房东和租客之间的协调关系,这个就是中介者,落实到具体的代码中呢,就像我们的Controller可能会依赖很多的Service层面的东西,在这里的代码都会注入很多Service,导致依赖很严重,耦合较高,并且多的情况下会使得Controller的代码看起来并不是那么简洁,而中介者模式,恰恰可以解决这一痛楚,降低Controller和Service层面的依赖,通过一个接口,来实现Controller调用业务层面的操作,在中介者体系下,我们更多的是关注我们自身的业务,以及在业务之中,我们如何构建我们的业务模型,以及每个业务需要做的事件处理操作即可,换做以往的开发模式,我们需要依赖Bussiness A,B,C三个Service。在引入了Mediator中介者模式,我们的Controller只需要注入并引入IMediator的对象,即可实现调用对应的A,B,C的业务操作。接下来,我们一起看看,如何设计以及如何使用。

设计

首先我是一名使用c#开发多于Java开发的人员,所以代码的编写风格,更多的是趋向于c#的写法,诸位请见谅,同时在中介者模式,Mediator在c#那边有一个成熟的框架,并且已经很多人在使用,在这里我只是简单的实现一个Java的一个版本,诸位,请莫见怪。整体的设计如下,在自动装配,3以前的版本和以后的版本是一样的,踩了个坑,以前老的方式是META-INF下面创建一个spring.factories,里面写入你自动配置的类,在新版本则是在META-INF下面创建一个spring文件夹,里面创建一个org.springframework.boot.autoconfigure.AutoConfiguration.imports的文件,在这里去写你要自动配置的类的路径即可,多个是多行,一行一个。


看上面的目录结构,我们可以看到有三个注解,第一个注解代表某个类是一个CommandHandler,这个类必须实现ICommahdHandler的接口,或者IEmptyCommandHandler接口,接口定义如下,里面都只是一个Handle的方法,ICommandHandler的泛型T必须实现IRequest的接口,这个代表是方法的入参,同时IRequest的泛型R代表是Handle的返回值,这个可以理解为,我们一个业务Handle的请求和响应。而下面的IEmptyCommandHandler是一个无返回值但是有入参的一个操作接口,其实这个可以用ApplicationEvent来代替也是没问题的,同时在C#的Mediator也提供了Publish的方法 ,这个也是没有返回值,其用法和Application.publishEvent是一样的,所以我在代码里仅定义了接口,并没有去实现。当我们自己实现了CommandHandler的接口以及请求和响应的时候,在Controller我们就只需要定义一个IMediator的对象即可,Autowired获取就行了。

packagecom.mediator.Handler;importcom.mediator.Request.IRequest;importjava.util.concurrent.Future;public interface ICommandHandler<T extends IRequest<R>,R>{

R Handle(T request);
}
packagecom.mediator.Handler;importcom.mediator.Request.IEmptyRequest;public interface IEmptyCommandHandler<T extends IEmptyRequest>{voidHandle(T request);
}
packagecom.mediator.Request;public interface IRequest<R>{

}
public interfaceIEmptyRequest {
}

这里是MediatorComfiguration的具体代码,我们需要获取到ApplicationContext上下文对象,在执行我们的InjectMediator的方法的时候,我们会先去找看有没有使用EnableCommandHandler注解的类对象,以此来判断有没有开启中介者模式并且使用,然后我们这个注解定义了Path,这是一个String,保存我们需要扫描的包路径,如果路径不为空,就获取默认的BeanFactory对象,我们需要来动态的注入我们的实现的各种Handler到容器以及PipeLine,在scanCommandHandlerClasses方法里,我们去根据包路径去扫描这个包下面,使用了CommandHandler注解的所有的类,最终获取到了一个BeanDefinition集合,我们获取到了Bean的ClassName作为Bean的名字,同时设置生命周期是request的,注入到容器里面,下面的是获取PipeLine注解的实现类的,标记这个注解代表的是Handler接口的Aop实现对象,也是定义了一个是空的Handler的Aop和CommandHandler的Aop,三个方法分别代表,在执行Handler之前,之后,以及出现异常的时候的调用。

public interface IPipeline<T,R>{void BeforeForHandler(IRequest<R>para);voidAfterForHandler(R res);voidHandlerException(Exception ex);
}
public interface IEmptyPipeline<T>{voidBeforeForHandler(IEmptyRequest para);voidAfterForHandler();voidHandlerException(Exception ex);
}
packagecom.mediator;importcom.mediator.Annotations.CommandHandler;importcom.mediator.Annotations.EnableCommandHandler;importcom.mediator.Annotations.PipeLine;importcom.mediator.Mediator.IMediator;importcom.mediator.Mediator.impl.Mediator;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.beans.factory.config.BeanDefinition;importorg.springframework.beans.factory.support.DefaultListableBeanFactory;importorg.springframework.context.ApplicationContext;importorg.springframework.context.ConfigurableApplicationContext;import org.springframework.context.annotation.*;importorg.springframework.core.annotation.AnnotationUtils;importorg.springframework.core.type.filter.AnnotationTypeFilter;importjava.util.Set;

@Configuration
public classMediatorConfiguration {

@Autowired
privateApplicationContext context;publicMediatorConfiguration()
{

}
@Bean
publicIMediator InjectMediator()
{
var enable
=context.getBeansWithAnnotation(EnableCommandHandler.class);if (!enable.isEmpty())
{
var application
=enable.values().iterator().next();
EnableCommandHandler handler
= AnnotationUtils.findAnnotation(application.getClass(), EnableCommandHandler.class);if (handler!=null)
{
var path
=handler.path();if (!path.isEmpty())
{ConfigurableApplicationContext configurableApplicationContext
=(ConfigurableApplicationContext) context;
DefaultListableBeanFactory factory
=(DefaultListableBeanFactory)configurableApplicationContext.getBeanFactory();
var allCommandClass
=scanCommandHandlerClasses(path);if (!allCommandClass.isEmpty())
{
for(BeanDefinition item:allCommandClass)
{
var name
=item.getBeanClassName();
item.setScope(
"request");
factory.registerBeanDefinition(name,item);
}
}
var pipeClass
=scanPipeLineClasses(path);if (!pipeClass.isEmpty())
{
for(BeanDefinition item:pipeClass)
{
var name
=item.getBeanClassName();
item.setScope(
"request");
factory.registerBeanDefinition(name,item);
}
}
}
}
}
return newMediator();
}
private Set<BeanDefinition>scanCommandHandlerClasses(String basePackage) {
AnnotationConfigApplicationContext context
= newAnnotationConfigApplicationContext();
ClassPathBeanDefinitionScanner scanner
= new ClassPathBeanDefinitionScanner(context,true);//添加过滤条件,只扫描带有 @CommandHandler 注解的类 scanner.addIncludeFilter(new AnnotationTypeFilter(CommandHandler.class));//扫描指定包路径 Set<BeanDefinition> candidates =scanner.findCandidateComponents(basePackage);returncandidates;
}
private Set<BeanDefinition>scanPipeLineClasses(String basePackage) {

AnnotationConfigApplicationContext context
= newAnnotationConfigApplicationContext();
ClassPathBeanDefinitionScanner scanner
= new ClassPathBeanDefinitionScanner(context,true);//添加过滤条件,只扫描带有 @PipeLine 注解的类 scanner.addIncludeFilter(new AnnotationTypeFilter(PipeLine.class));//扫描指定包路径 Set<BeanDefinition> candidates =scanner.findCandidateComponents(basePackage);returncandidates;
}
}

IMediator里就提供了两个可以用的方法,有返回值和无返回值的Send方法,分别对应执行的是ICommandHandler和IEmptyCommandHandler两个接口的实现,publish方法没有实现就是因为可以用ApplicationEvent去实现,可以看下面的实现,我们会先获取入参的Class对象,这里是IRequest是一个泛型,所以我们获取它第一个泛型的ParameterType,在下面获取泛型的具体的Class对象,这样我们就从Bean里面拿我们之前注入的自己实现的ICommandHandler的ResolvableType,同时获取对应的PipeLine的ResolvableType,在下面根据ResolvableType拿到Bean的Provider,同时判断如果存在这个Bean就赋值给我们的pipe对象,如果不存在就为null,在下面我们就去获取我们的ICommandHandler的对象,同理去拿Provider,根据上面我们的判断,是否存在PipeLine的Aop,如果存在,调用前就去调用BeforeForHandler方法,传入入参,继续往下走就是调用Handle的方法,获取返回值,在调用AfterForHandler的方法,同时下面有异常的时候,会调用异常的方法。下面的EmptyHandler无返回值的调用和有返回值的同出一辙。接下来我们看看,具体在代码中如何使用。

public interfaceIMediator {<R> R Send(IRequest<R> value) throwsNoSuchMethodException, InvocationTargetException, IllegalAccessException;<T>void Publish(INotification<T>notification);voidSend(IEmptyRequest val);
}
packagecom.mediator.Mediator.impl;importcom.mediator.Aop.IEmptyPipeline;importcom.mediator.Aop.IPipeline;importcom.mediator.Handler.ICommandHandler;importcom.mediator.Handler.IEmptyCommandHandler;importcom.mediator.Mediator.IMediator;importcom.mediator.Request.IEmptyRequest;importcom.mediator.Request.INotification;importcom.mediator.Request.IRequest;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.ApplicationContext;importorg.springframework.core.ResolvableType;importorg.springframework.stereotype.Component;importjava.lang.reflect.InvocationTargetException;importjava.lang.reflect.ParameterizedType;
@Component
public class Mediator implementsIMediator {
@Autowired
privateApplicationContext context;publicMediator()
{
}
@Override
public <R> R Send(IRequest<R> values) throwsNoSuchMethodException, InvocationTargetException, IllegalAccessException {if (values!=null)
{
var valuesClass
=values.getClass();
var parameter
=valuesClass.getGenericInterfaces()[0];if (parameter instanceofParameterizedType)
{
Class generaType
=(Class) ((ParameterizedType) parameter).getActualTypeArguments()[0];
var commandresolvableType
=ResolvableType.forClassWithGenerics(ICommandHandler.class,valuesClass,generaType);
var pipeLineresolvableType
=ResolvableType.forClassWithGenerics(IPipeline.class,valuesClass,generaType);
IPipeline pipe
=null;if (pipeLineresolvableType!=null)
{
var pipeLineprovider
=context.getBeanProvider(pipeLineresolvableType);if (pipeLineprovider!=null)
{
var object
=pipeLineprovider.getIfAvailable();
pipe
=object!=null?(IPipeline)object:null;
}
}
boolean bPipeIsNull=pipe==null?true:false;if (commandresolvableType!=null)
{
var provider
=context.getBeanProvider(commandresolvableType);
var handlerobject
=provider.getIfAvailable();
ICommandHandler handler
=handlerobject!=null?(ICommandHandler)handlerobject:null;if (!bPipeIsNull)
{
pipe.BeforeForHandler(values);
}
if (handler!=null)
{
try{
R res
=(R)handler.Handle(values);if (!bPipeIsNull)
{
pipe.AfterForHandler(res);
}
returnres;
}
catch(Exception ex)
{
if (!bPipeIsNull) {
pipe.HandlerException(ex);
}
}
}
}
}
}
return null;
}

@Override
public <T> void Publish(INotification<T>notification)
{

}

@Override
public voidSend(IEmptyRequest val)
{
if (val!=null)
{
var classObj
=val.getClass();
var commandresolvableType
=ResolvableType.forClassWithGenerics(IEmptyCommandHandler.class,classObj);
var pipeLineresolvableType
=ResolvableType.forClassWithGenerics(IEmptyPipeline.class,classObj);
IEmptyPipeline pipe
=null;if (pipeLineresolvableType!=null)
{
var pipeLineprovider
=context.getBeanProvider(pipeLineresolvableType);if (pipeLineprovider!=null)
{
var object
=pipeLineprovider.getIfAvailable();
pipe
=object!=null?(IEmptyPipeline)object:null;
}
}
boolean bPipeIsNull=pipe==null?true:false;if (commandresolvableType!=null)
{
var provider
=context.getBeanProvider(commandresolvableType);
var handlerobject
=provider.getIfAvailable();
IEmptyCommandHandler handler
=handlerobject!=null?(IEmptyCommandHandler)handlerobject:null;if (handler!=null)
{
try{if (!bPipeIsNull) {
pipe.BeforeForHandler(val);
}
handler.Handle(val);
if (!bPipeIsNull) {
pipe.AfterForHandler();
}
}
catch(Exception ex)
{
if (!bPipeIsNull) {
pipe.HandlerException(ex);
}
}
}
}
}
}
}

使用

在做的测试项目里,需要在启动类先标记EnableCommandHandler注解,设置存放CommandHandler和Aop的包的路径,然后,下面第二段代码是我定义的一些测试的Request和Response,需要实现特定的接口标记是Request,并且指名Response。

@SpringBootApplication
@EnableAsync
@EnableCommandHandler(path
= "com.example.streamtransport.Handler")public classStreamTransportApplication {public static voidmain(String[] args) {

SpringApplication.run(StreamTransportApplication.
class, args);
}

}
public class TestARequest implements IRequest<TestAResponse>{

}
public classTestAResponse {

}
public class EmptyRequest implementsIEmptyRequest {
}
public class BussinessRequest implements IRequest<BussinessResponse>{
}
public classBussinessResponse {
}

下面这段代码是实现了自定义Handler,上面标记我们是一个CommandHandler,无返回值的Handler和有返回值的一样,不过不同的实现接口而已,同时在我们的CommandHandler,可以依赖IMediator,我们也可以在ACommandHandler调用BCommandHandler,在这样的模式下,我们只关注我们的入参和响应模型,

@CommandHandlerpublic class TestHandler implements ICommandHandler<TestARequest, TestAResponse>{publicTestHandler()
{
var t
="1";
}
@Override
publicTestAResponse Handle(TestARequest request) {return newTestAResponse();
}
}
@CommandHandler
public class EmptyCommandHandler implements IEmptyCommandHandler<EmptyRequest>{
@Override
public voidHandle(EmptyRequest request)
{
var a
=1;
}
}
@CommandHandler
public class BussinessHandler implements ICommandHandler<BussinessRequest, BussinessResponse>{
@Autowired
privateIMediator mediator;
@Override
publicBussinessResponse Handle(BussinessRequest request) {
mediator.Send(
newEmptyRequest());return newBussinessResponse();
}
}

下面这段代码,则是我们自己实现的Aop管道,实现三个方法,并且输出每一步骤执行的具体的动作。接下来,我们跑起来,看看输出。

@PipeLinepublic class Pipeline implements IPipeline<TestARequest, TestAResponse>{
@Override
public void BeforeForHandler(IRequest<TestAResponse>para)
{
System.out.println(
"BeforeForHandler");
}

@Override
public voidAfterForHandler(TestAResponse res)
{
System.out.println(
"AfterForHandler");
}

@Override
public voidHandlerException(Exception ex)
{
System.out.println(
"HandlerException");
}
}

@PipeLine
public class EmptyPipeLine implements IEmptyPipeline<EmptyRequest>{
@Override
public voidBeforeForHandler(IEmptyRequest para) {

System.out.println(
"EmptyBeforeForHandler");
}

@Override
public voidAfterForHandler() {
System.out.println(
"EmptyAfterForHandler");
}

@Override
public voidHandlerException(Exception ex) {
System.out.println(
"EmptyException");
}
}

下面的图片中,GetHellos方法刚开始调用了一个Send,传入了一个EmptyRequest对象,输出了第一次的EmptyBeforeForHandler,EmptyAfterForHandler,在后面我们调用了有返回值的传入了TestARequest对象,输出了BeforeForHandler和AfterForHandler,由于在上面的Aop我们没有定义BussinessHandler对应的PipeLine,所以在这里输出是没有Bussiness的输出,但是我们在BussinessHandler里面有Send了一次EmptyRequest,所以最后又输出了一边Empty的Aop的输出。

结束

以上便是我今天的分享的内容,Java大腿子请莫见怪,代码能跑就行,并没有做更多的优化,仅仅作为一个分享的例子,谢谢大家,代码已上传至Gitee,

Mediator:https://gitee.com/cxd199645/mediator.git

MediatorDemo:https://gitee.com/cxd199645/mediator-demo.git

有什么问题可以一起讨论。

1、准备材料

开发板(
正点原子stm32f407探索者开发板V2.4

STM32CubeMX软件(
Version 6.10.0

野火DAP仿真器

keil µVision5 IDE(
MDK-Arm

ST-LINK/V2驱动

XCOM V2.6串口助手

2、实验目标

使用STM32CubeMX软件配置STM32F407开发板的
FSMC实现以轮询或DMA的方式读写IS62WV51216(SRAM)芯片

3、轮询方式读写SRAM

3.0、前提知识

关于FSMC的内容读者可阅读“
STM32CubeMX教程22 FSMC - 8080并行接口TFT-LCD驱动
”实验“3.0、前提知识”小节

本实验使用的SRAM芯片为IS62WV51216,其为16位宽512K容量的静态随机存取存储器,开发板使用FSMC Bank 1-NOR/PSRAM3,片选信号为NE3(PG10),地址线A[0:18],数据线D[0:15]来控制该存储芯片,如下图所示为该存储芯片的硬件原理图

FSMC的地址线是按照字节寻址的,
因此19根地址线的寻址范围应该为0x0 0000~0x7 FFFF
(2^19=524288=0x8 0000)

但是由于该存储器为16位(2个字节),因此
其实际容量应该为2*512KB=1024KB,其需要的寻址范围为0x0 0000~0xF FFFF

再考虑到FSMC Bank 1-NOR/PSRAM3起始地址为0x6800 0000,因此如果要访问IS62WV51216芯片全部的1024KB数据的地址范围应该为0x6800 0000~0x680F FFFF

19根地址线做不到,那怎么办?

解决方法就是将16位宽分为高字节和低字节,通过FSMC的字节控制引脚FSMC_NBL0/1实现全部1024KB存储空间的访问

3.1、CubeMX相关配置

3.1.0、工程基本配置

打开STM32CubeMX软件,单击ACCESS TO MCU SELECTOR选择开发板MCU(选择你使用开发板的主控MCU型号),选中MCU型号后单击页面右上角Start Project开始工程,具体如下图所示

开始工程之后在配置主页面System Core/RCC中配置HSE/LSE晶振,在System Core/SYS中配置Debug模式,具体如下图所示

详细工程建立内容读者可以阅读“
STM32CubeMX教程1 工程建立

3.1.1、时钟树配置

系统时钟使用8MHz外部高速时钟HSE,HCLK、PCLK1和PCLK2均设置为STM32F407能达到的最高时钟频率,具体如下图所示

3.1.2、外设参数配置

本实验需要需要初始化开发板上KEY1和KEY0两个用户按键,具体配置步骤请阅读“
STM32CubeMX教程3 GPIO输入 - 按键响应

本实验需要需要初始化USART1作为输出信息渠道,具体配置步骤请阅读“
STM32CubeMX教程9 USART/UART 异步通信

本实验写入SRAM数据时使用到了STM32的随机数RNG功能,读者直接在Security/RNG中将其激活即可

单击Pinout & Configuration页面左边Connectivity/FSMC选项,在右边的Mode下点开NORFIash/PSRAM/SRAM/ROM/LCD3选项卡(因为SRAM使用的是使用FSMC Bank 1-NOR/PSRAM3),然后按照下面顺序配置

  1. 选择片选信号NE3
  2. 内存类型为SRAM
  3. FSMC地址线19位
  4. 数据Data为16位宽度(D0-D15)
  5. 使能字节控制Byte enable

下方的读写时序参数配置可阅读“
STM32CubeMX教程22 FSMC - 8080并行接口TFT-LCD驱动
”实验“3.0、前提知识”小节,具体配置如下图所示

配置完成之后
请读者对照STM32CubeMX中配置的默认功能引脚是否和开发板硬件原理图每个引脚对应,防止出现默认功能引脚与开发板设计的引脚不一致的现象

3.1.3、外设中断配置

轮询方式读写SRAM无需配置任何中断

3.2、生成代码

3.2.0、配置Project Manager页面

单击进入Project Manager页面,在左边Project分栏中修改工程名称、工程目录和工具链,然后在Code Generator中勾选“Gnerate peripheral initialization as a pair of 'c/h' files per peripheral”,最后单击页面右上角GENERATE CODE生成工程,具体如下图所示

详细Project Manager配置内容读者可以阅读“
STM32CubeMX教程1 工程建立
”实验3.4.3小节

3.2.1、外设初始化调用流程

请阅读“
STM32CubeMX教程22 FSMC - 8080并行接口TFT-LCD驱动
”实验3.2.1小节

3.2.2、外设中断调用流程

轮询方式读写SRAM无需配置任何中断

3.2.3、添加其他必要代码

在main.c文件中添加SRAM读写测试函数,具体函数源代码如下所示

/*用HAL函数写入数据*/
void SRAM_WriteByFunc(void)
{
    //1.写入字符串
    uint32_t *pAddr = (uint32_t *)(SRAM_ADDR_BEGIN);	//给指针赋值
    uint8_t strIn[] = "Moment in UPC";
    uint16_t dataLen = sizeof(strIn); //数据长度,字节数,包括最后的结束符'\0’
    if(HAL_SRAM_Write_8b(&hsram3, pAddr, strIn, dataLen) == HAL_OK)
    {
        printf("Write string at 0x6800 0000:");
        printf("%s\r\n",strIn);
    }
 
    //2.写入一个随机数
    uint32_t num=0;
    pAddr=(uint32_t *)(SRAM_ADDR_BEGIN+256);	//指针重新赋值
    HAL_RNG_GenerateRandomNumber(&hrng, &num);	//产生32位随机数
    if(HAL_SRAM_Write_32b(&hsram3, pAddr, &num, 1) == HAL_OK)
    {
        printf("Write 32b number at 0x6800 0100");
        printf("0x%x\r\n", num);	
    }
    printf("-----------------------------------------\r\n");
}
 
/*用HAL函数读取数据*/
void SRAM_ReadByFunc(void)
{
    //1.读取字符串
    uint32_t *pAddr = (uint32_t *)(SRAM_ADDR_BEGIN);	//给指针赋值
    uint8_t strOut[30];
    uint16_t dataLen = 30;
    if(HAL_SRAM_Read_8b(&hsram3, pAddr, strOut, dataLen) == HAL_OK)
    {
        printf("Read string at 0x6800 0000:");
        printf("%s\r\n", strOut); 
    }
 
    //2.读取一个uint32_t数
    uint32_t num=0;
    pAddr=(uint32_t *)(SRAM_ADDR_BEGIN+256);	//指针重新赋值,指向一个新的地址
    if(HAL_SRAM_Read_32b(&hsram3, pAddr, &num, 1) == HAL_OK)
    {
        printf("Read 32b number at 0x6800 0100:");
        printf("0x%x\r\n", num);
    }
    printf("-----------------------------------------\r\n");
}

在main.c文件中添加使用到的SRAM起始/中间/结束地址,并对定义的函数声明,源代码如下所示

/*定义SRAM地址*/
#define SRAM_ADDR_BEGIN		0x68000000UL //Bank1 子区3的 SRAM起始地址
#define SRAM_ADDR_HALF		0x68080000UL //SRAM 中间地址 512K字节
#define SRAM_ADDR_END	    0x680FFFFFUL //SRAM 结束地址 1024K字节
 
/*读写测试函数声明*/
void SRAM_ReadByFunc(void);
void SRAM_WriteByFunc(void);

最后在主函数主循环while(1)中使用用户按键调用SRAM读写测试函数即可,源代码如下所示

/*KEY1被按下*/
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
{
    HAL_Delay(50);
    if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
    {
        //SRAM写测试函数
        SRAM_WriteByFunc();
        while(!HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin));
    }
}
 
/*KEY0被按下*/
if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
{
    HAL_Delay(50);
    if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
    {
        //SRAM读测试函数
        SRAM_ReadByFunc();
        while(!HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin));
    }
}

4、常用函数

/*向SRAM中写8位缓存数据*/
HAL_StatusTypeDef HAL_SRAM_Write_8b(SRAM_HandleTypeDef *hsram, uint32_t *pAddress, uint8_t *pSrcBuffer, uint32_t BufferSize)
 
/*从SRAM中读8位缓存数据*/
HAL_StatusTypeDef HAL_SRAM_Read_8b(SRAM_HandleTypeDef *hsram, uint32_t *pAddress, uint8_t *pDstBuffer, uint32_t BufferSize)
 
/*向SRAM中写16位缓存数据*/
HAL_StatusTypeDef HAL_SRAM_Write_16b(SRAM_HandleTypeDef *hsram, uint32_t *pAddress, uint8_t *pSrcBuffer, uint32_t BufferSize)
 
/*从SRAM中读16位缓存数据*/
HAL_StatusTypeDef HAL_SRAM_Read_16b(SRAM_HandleTypeDef *hsram, uint32_t *pAddress, uint8_t *pDstBuffer, uint32_t BufferSize)
 
/*向SRAM中写32位缓存数据*/
HAL_StatusTypeDef HAL_SRAM_Write_32b(SRAM_HandleTypeDef *hsram, uint32_t *pAddress, uint8_t *pSrcBuffer, uint32_t BufferSize)
 
/*从SRAM中读32位缓存数据*/
HAL_StatusTypeDef HAL_SRAM_Read_32b(SRAM_HandleTypeDef *hsram, uint32_t *pAddress, uint8_t *pDstBuffer, uint32_t BufferSize)
 
/*向SRAM中写32位缓存数据*/
HAL_StatusTypeDef HAL_SRAM_Write_DMA(SRAM_HandleTypeDef *hsram, uint32_t *pAddress, uint8_t *pSrcBuffer, uint32_t BufferSize)
 
/*从SRAM中读32位缓存数据*/
HAL_StatusTypeDef HAL_SRAM_Read_DMA(SRAM_HandleTypeDef *hsram, uint32_t *pAddress, uint8_t *pDstBuffer, uint32_t BufferSize)

5、烧录验证

烧录程序,开发板上电后打开串口助手,按下KEY0按键从SRAM中指定位置读取数据,发现读取到错误乱码信息;按下KEY1按键将准备好的数据写入SRAM指定位置,再次按下KEY0按键从该指定位置读取数据,发现和我们写入的数据一致,可重复多次尝试,发现读取的信息均和写入信息一致,如下图所示为串口输出详细信息

6、DMA方式读写SRAM

6.1、前提知识

读写SRAM还可以使用DMA的方式进行,但是由于SRAM属于存储设备,因此读写SRAM的DMA方向应该为内存到内存

在“
STM32CubeMX教程12 DMA 直接内存读取
”我们提到只有DAM2的8个通道可以实现从存储器到存储器这种传输模式,由于该模式比较特殊,因此读者可以发现不同于其他外设DMA的配置,在FSMC选项卡我们配置SRAM的页面中根本没有DMA选项卡可以设置,
因此想要设置从存储器到存储器这种传输模式的DMA必须在System Core/DMA选项卡中设置

另外在其他外设设置DMA时,DMA流与外设关联的函数__HAL_LINKDMA()会被自动生成在HAL_xxx_MspInit()函数中

但是如果DMA为存储器到存储器这种传输模式,则生成的工程代码中不会自动关联DMA流与外设(他也不知道该如何关联),
因此在DMA及外设均初始化完毕之后,需要用户手动增加__HAL_LINKDMA()函数将外设与DMA流关联

6.2、CubeMX相关配置

请读者先按照本实验“3.1、CubeMX相关配置” 小节配置RCC、SYS、用户按键、串口USART1和FSMC模式和基本参数

如果读者对DMA参数不理解,请阅读“
STM32CubeMX教程12 DMA 直接内存读取
”实验,
然后按下方顺序配置DMA参数

  1. 单击Pinout & Configuration页面左边System Core/DMA选项卡
  2. 在Configuration中选择MemToMem
  3. 单击下方的增加按钮选择增加DMA请求
  4. 单击增加的DMA请求
  5. 在下方对其参数进行配置,模式为Normal
  6. 源/目标内存地址均递增
  7. 数据宽度选择word(因为HAL库提供的DMA写入SRAM函数数据为32位的)

其他参数默认,具体配置参看下图

在Pinout & Configuration页面左边System Core/NVIC中勾选DMA2 Stream0 全局中断,然后选择合适的中断优先级即可

6.3、DMA中断调用流程

DMA触发中断,其回调函数是一个函数指针的形式,在外设使用DMA启动传输的时候会将外设对应的中断回调函数赋值给DMA中断回调函数指针,具体请阅读“
STM32CubeMX教程12 DMA 直接内存读取
”实验实验3.2.2小节,流程大致一致

这里读者只需知道,
在STM32CubeMX开启FSMC配置的SRAM DMA中断之后,在工程代码中使用HAL_SRAM_Read_DMA() / HAL_SRAM_Write_DMA()函数传输完毕之后,都会调用HAL_SRAM_DMA_XferCpltCallback()虚函数,用户重新实现该虚函数即可

由于以DMA方式读和写SRAM传输完成的回调函数为同一个,因此用户可以自己设定标志位,从而可以在中断回调函数中判断是读完成还是写完成

6.4、添加其他必要代码

配置工程并单击页面右上角GENERATE CODE生成工程

在main.c文件中FSMC初始化完毕之后添加DMA流与外设关联的函数

//将外设与DMA流关联
__HAL_LINKDMA(&hsram3, hdma, hdma_memtomem_dma2_stream0);

在main.c文件中增加以DMA方式读写SRAM的测试函数,源代码如下

/*以DMA方式写入数据*/
void SRAM_WriteDMA(void)													
{
    printf("Write 32bit array by DMA:");
    uint32_t Value=3000;	
    for(uint8_t i=0; i<COUNT; i++)
    {
        txBuffer[i] = Value;
        printf("%d,",Value);
        Value += 6;
    }
    printf("\r\n");
    //DMA传输方向,1=write, 0=read
    DMA_Direction=1;					
    //表示DMA正在传输,1=working, 0=idle	
    DMA_Busy=1;																			
    //给指针赋值
    uint32_t *pAddr_32b=(uint32_t *)(SRAM_ADDR_BEGIN);			
    //DMA方式写入SRAM	
    HAL_SRAM_Write_DMA(&hsram3, pAddr_32b, txBuffer, COUNT);	
}
 
/*以DMA方式读取数据*/
void SRAM_ReadDMA(void)														
{
    printf("Read 32bit array by DMA\r\n");
    DMA_Direction=0;													
    DMA_Busy=1;			
    uint32_t *pAddr_32b=(uint32_t *)(SRAM_ADDR_BEGIN);	
    //以DMA方式读取SRAM
    HAL_SRAM_Read_DMA(&hsram3, pAddr_32b, rxBuffer, COUNT);
}

在main.c文件头部增加函数声明及使用到的一些变量定义,源代码如下

/*函数声明*/
void SRAM_WriteDMA(void);
void SRAM_ReadDMA(void);
 
/*变量定义*/
#define COUNT 5                      //缓存区数据个数
uint32_t txBuffer[COUNT];            //DMA发送缓存区
uint32_t rxBuffer[COUNT];            //DMA接收缓存区
uint8_t DMA_Direction = 1;           //DMA传输方向,1=write, 0=read
uint8_t DMA_Busy = 0;                //DMA工作状态,1=busy, 0=idle

最后在主循环中使用用户按键调用SRAM读写测试函数即可,源代码如下所示

/*KEY1被按下*/
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
{
    HAL_Delay(50);
    if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
    {
        //SRAM DMA写测试函数
        SRAM_WriteDMA();
        while(!HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin));
    }
}
 
/*KEY0被按下*/
if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
{
    HAL_Delay(50);
    if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
    {
        //SRAM DMA读测试函数
        SRAM_ReadDMA();
        while(!HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin));
    }
}

6.5、实验现象

烧录程序,开发板上电后打开串口助手,按下KEY0按键以DMA方式从SRAM中指定位置读取数据,发现读取到错误的5个数据;按下KEY1按键将3000,3006,3012,3018,3024五个数据以DMA方式写入SRAM指定位置,再次按下KEY0按键以DMA方式从SRAM中指定位置读取数据,发现和我们写入的数据一致,整个过程读者也可以发现每次以DMA方式写入/读取SRAM完成之后都会进入DMA传输完成回调函数中,如下图所示为整个过程串口输出的详细信息

参考资料

STM32Cube高效开发教程(基础篇)

配置POM

只需要加一个依赖,并且要注意,swagger3在springboot2.5版本以上会出现问题

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>3.0.0</version>
        </dependency>

如果高于2.5版本会报错:

org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException

解决方法是降低spring的版本到2.5.x以下,或者是降低swagger版本到3以下,或者是在SwaggerConfig注解上标注@EnableWebMvc

配置例子

配置SwaggerConfig

@Configuration
@EnableSwagger2
@Slf4j
@EnableWebMvc
public class SwaggerConfig {

    @Bean
    public Docket docket(Environment environment) {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo()) // 文档基础配置
                .select()
                .paths(PathSelectors.regex("(?!/error.*).*"))  //加这行去除掉basic error controller接口
                .build();

    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("xx平台接口文档")
                .build();
    }

}

配置实体类

@ApiModel(value = "UsersDTO", description = "UsersDTO实体类")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserDTO {

    @ApiModelProperty(value = "First name", example = "Jean")
    private String firstname;

    @ApiModelProperty(value = "Last name", example = "ab")
    private String lastname;

    @ApiModelProperty(value = "CardInfo")
    private CardInfo cardInfo;

}

可以看到这个类存在CardInfo嵌套类,对嵌套类的配置同上:

@ApiModel(value = "CardInfo", description = "CardInfo实体类")
@Data
public class CardInfo {

    @ApiModelProperty(value = "cardName", example = "card")
    String cardName;
}

注意:实体类的字段都需要有get方法,不然会失效,这里统一使用lombok的@Data解决

配置Controller类

@RestController
@Api(tags = "用户管理接口")
@RequestMapping("/users")
public class UsersController {

    @Autowired
    private UsersService usersService;

    @ApiOperation(value = "用户注册",notes = "传入用户信息进行注册")
    @PostMapping(value = "/register")
    public AjaxResult<Users> register(@RequestBody Users users) throws IOException {
        usersService.save(users);
        return AjaxResult.success(users);
    }

}

这里面的返回值AjaxResult需要定义好泛型,在返回值初定义类型

AjaxResult类

@ApiModel("API通用返回数据")
@Data
public class AjaxResult<T> {

    @ApiModelProperty(value = "状态码", example = "200")
    private final int code;

    @ApiModelProperty(value = "返回消息", example = "success")
    private final String message;

    @ApiModelProperty("数据对象")
    private final T data;

    public AjaxResult(int code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    public T getData() {
        return data;
    }

    public static <T> AjaxResult<T> success() {
        return new AjaxResult<>(ResultCodeEnum.SUCCESS.getCode(), ResultCodeEnum.SUCCESS.getMessage(), null);
    }

    public static <T> AjaxResult<T> success(String message) {
        return new AjaxResult<>(ResultCodeEnum.SUCCESS.getCode(), message, null);
    }

    public static <T> AjaxResult<T> success(T data) {
        return new AjaxResult<>(ResultCodeEnum.SUCCESS.getCode(), ResultCodeEnum.SUCCESS.getMessage(), data);
    }

    public static <T> AjaxResult<T> success(String message, T data) {
        return new AjaxResult<>(ResultCodeEnum.SUCCESS.getCode(), message, data);
    }

    public static <T> AjaxResult<T> failed() {
        return new AjaxResult<>(ResultCodeEnum.FAILED.getCode(), ResultCodeEnum.FAILED.getMessage(), null);
    }

    public static <T> AjaxResult<T> failed(String message) {
        return new AjaxResult<>(ResultCodeEnum.FAILED.getCode(), message, null);
    }

    public static <T> AjaxResult<T> failed(ResultCodeEnum resultCodeEnum) {
        return new AjaxResult<>(resultCodeEnum.getCode(), resultCodeEnum.getMessage(), null);
    }

    public static <T> AjaxResult<T> failed(String message, T data) {
        return new AjaxResult<>(ResultCodeEnum.FAILED.getCode(), message, data);
    }

    public static <T> AjaxResult<T> failed(ResultCodeEnum resultCodeEnum, T data) {
        return new AjaxResult<>(resultCodeEnum.getCode(), resultCodeEnum.getMessage(), data);
    }
}

效果

image-20240123200557956

swagger有关的常用注解

  1. @Api
    注解

    @Api
    注解用于描述整个 API,通常放在控制器类上,一般使用tags注解就可以

    @Api(tags = "User API")
    @RestController
    @RequestMapping("/api/users")
    public class UserController {
        // ...
    }
    
  2. @ApiOperation
    注解

    @ApiOperation
    注解用于描述具体的 API 操作,通常放在控制器方法上

    @ApiOperation(
        value = "Get user by ID",
        notes = "Get user details by providing user ID"
    )
    @GetMapping("/{userId}")
    public ResponseEntity<User> getUserById(@PathVariable Long userId) {
        // Implementation to get user by ID
    }
    
  3. @ApiParam
    注解

    @ApiParam
    注解用于描述方法参数,提供参数的名称、描述等信息。

    @GetMapping("/{userId}")
    public ResponseEntity<User> getUserById(
        @ApiParam(name = "userId", value = "ID of the user", required = true)
        @PathVariable Long userId) {
        // Implementation to get user by ID
    }
    
  4. @ApiResponse

    @ApiResponses
    注解

    这两个注解用于描述操作的响应信息,作用在方法上。

    @ApiResponses({
        @ApiResponse(code = 200, message = "Successful operation", response = String.class),
        @ApiResponse(code = 404, message = "User not found", response = String.class),
    })
    @GetMapping("/{userId}")
    public ResponseEntity<User> getUserById(@PathVariable Long userId) {
        // Implementation to get user by ID
    }
    
  5. @ApiModel

    @ApiModelProperty
    注解

    这两个注解用于描述数据模型,通常放在实体类上。
    其中,下述的example可以实现在swagger页面调接口的默认值,并且如果导入到如eolink这种api管理工具,这个默认值也会填充进去。

    @ApiModel(description = "User information")
    public class User {
        @ApiModelProperty(value = "User ID", example = "ab")
        private Long id;
    
        @ApiModelProperty(value = "User name", example = "cd")
        private String name;
    
        // Getters and setters
    }
    
  6. @ApiIgnore
    注解

    @ApiIgnore
    注解用于标记不想在 Swagger 文档中显示的类、方法。

    @ApiIgnore
    public class IgnoredController {
        // ...
    }
    

    上述的
    IgnoredController
    类及其所有方法将被忽略。

  7. @ApiParamImplicit
    注解

    @ApiParamImplicit
    注解用于表示参数,需要被包含在注解
    @ApiImplicitParams
    之内。

    @ApiImplicitParams({
        @ApiImplicitParam(name = "userId", value = "ID of the user", required = true, dataType = "long", paramType = "path"),
    })
    @GetMapping("/{userId}")
    public ResponseEntity<User> getUserById(
        @PathVariable Long userId) {
        // Implementation to get user by ID
    }
    

导出json格式的swagger文档

点击主页这个地方

image-20240123202246160

按F12,在源代码里面的v2-api-docs里面右键另存为

image-20240123202423948

输入名称和后缀进行保存

image-20240123202516635

导入eolink

点api向下的箭头,再选swagger

image-20240123202614038

导入成功后可以看到,传参和返回值都能被正确识别和导入,包括传参的默认值也有

image-20240123203040508