安卓CameraX的使用

news2024/11/24 1:41:49

如果要在安卓应用中操作相机,有三个库可以选:

  1. Camera(已废弃):Camera是安卓最早的包,目前已废弃,在Android 5.0(API 级别 21)的设备上操作相机可以选择该包,保证兼容性;
  2. Camera2:Camera2在Android 5.0(API 级别 21)开始提供,用于替代Camera,相比Camera,在性能、灵活性等方面有优势,但是使用起来更复杂;
  3. CameraX:CameraX是对Camera2的封装,是Jetpack的一个库,支持Android 5.0(API 级别 21)及更高版本。CameraX降低了操作相机的难度,大多数情况下,使用CameraX已足以完成需求,建议使用CameraX。

注:Jetpack是一个由多个库组成的套件,可帮助开发者遵循最佳做法、减少样板代码并编写可在各种Android版本和设备中一致运行的代码,让开发者可将精力集中于真正重要的编码工作。

谷歌的文档只提供了Kotlin的代码示例,根据Kotlin的代码示例和文档,本文提供Java的实现。

1. CameraX常见用例

您可以使用CameraX,借助名为“用例”的抽象概念与设备的相机进行交互。提供的用例如下:

  • 预览(Preview):在屏幕上查看相机画面;
  • 图片分析(ImageAnalysis):逐帧处理相机捕获到的每一帧画面;
  • 图片拍摄(ImageCapture):拍照;
  • 视频拍摄(VideoCapture):拍视频(和音频)。

CameraX允许同时使用Preview、VideoCapture、ImageAnalysis和ImageCapture各一个实例。此外:

  • 每个用例都可以单独使用。例如,应用可以在不使用预览的情况下录制视频;
  • 启用扩展后,只能保证能够使用ImageCapture和Preview的组合。根据OEM实现情况,可能无法同时添加ImageAnalysis;无法为VideoCapture用例启用扩展。如需了解详情,请参阅扩展参考文档;
  • 对于某些相机而言,在较低分辨率模式下可以支持的组合,在较高的分辨率下将无法支持,这具体取决于相机的功能;
  • 在相机硬件级别为FULL或更低的设备上,组合使用Preview、VideoCapture和ImageCapture或ImageAnalysis可能会迫使CameraX为Preview和VideoCapture复制相机的PRIV数据流。这种重复(称为数据流共享)可让您同时使用这些功能,但代价是增加了处理需求。因此,您可能会遇到略长的延迟时间和缩短的电池续航时间。

2. 代码实现

在build.gradle中添加相关依赖:

// camerax
def camerax_version = "1.3.0"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-video:${camerax_version}"
// 扩展,本文代码未使用
// implementation "androidx.camera:camera-extensions:${camerax_version}"

在AndroidManifest.xml添加相关权限:

<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

添加布局文件activity_camerax_preview.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <androidx.camera.view.PreviewView
        android:id="@+id/camerax_preview_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center">

                <Button
                    android:id="@+id/camerax_flash"
                    android:layout_width="110dp"
                    android:layout_height="70dp"
                    android:layout_marginStart="10dp"
                    android:layout_marginTop="5dp"
                    android:layout_marginEnd="10dp"
                    android:layout_marginBottom="5dp"
                    android:text="开灯" />

                <Button
                    android:id="@+id/camerax_switch_camera"
                    android:layout_width="110dp"
                    android:layout_height="70dp"
                    android:layout_marginStart="10dp"
                    android:layout_marginTop="5dp"
                    android:layout_marginEnd="10dp"
                    android:layout_marginBottom="5dp"
                    android:text="切换摄像头" />

            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center">

                <Button
                    android:id="@+id/camerax_image_capture_button"
                    android:layout_width="110dp"
                    android:layout_height="70dp"
                    android:layout_marginStart="10dp"
                    android:layout_marginTop="5dp"
                    android:layout_marginEnd="10dp"
                    android:layout_marginBottom="5dp"
                    android:text="拍照" />

                <Button
                    android:id="@+id/camerax_video_capture_button"
                    android:layout_width="110dp"
                    android:layout_height="70dp"
                    android:layout_marginStart="10dp"
                    android:layout_marginTop="5dp"
                    android:layout_marginEnd="10dp"
                    android:layout_marginBottom="5dp"
                    android:text="开始录制" />

                <Button
                    android:id="@+id/camerax_video_capture_pause_button"
                    android:layout_width="110dp"
                    android:layout_height="70dp"
                    android:layout_marginStart="10dp"
                    android:layout_marginTop="5dp"
                    android:layout_marginEnd="10dp"
                    android:layout_marginBottom="5dp"
                    android:text="暂停录制"
                    android:visibility="gone" />
            </LinearLayout>
        </LinearLayout>
    </RelativeLayout>
</RelativeLayout>

activity类:

package org.tao.hetools.activities;

import android.Manifest;
import android.content.ContentValues;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.util.Size;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import androidx.activity.ComponentActivity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.Preview;
import androidx.camera.core.TorchState;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.video.MediaStoreOutputOptions;
import androidx.camera.video.Quality;
import androidx.camera.video.QualitySelector;
import androidx.camera.video.Recorder;
import androidx.camera.video.Recording;
import androidx.camera.video.VideoCapture;
import androidx.camera.view.PreviewView;
import androidx.core.content.ContextCompat;

import com.google.common.util.concurrent.ListenableFuture;

import org.tao.hetools.R;
import org.tao.hetools.utils.PermissionUtils;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;

public class CameraxPreviewActivity extends ComponentActivity {
    private static final String TAG = "CameraxPreviewActivity";
    private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyyMMddHHmmssSSS");
    private static final Size SIZE = new Size(1080, 1920);

    private static final List<CameraSelector> CAMERAS = Arrays.asList(CameraSelector.DEFAULT_BACK_CAMERA,
            CameraSelector.DEFAULT_FRONT_CAMERA);

    private static final String START_RECORD = "开始录制";
    private static final String STOP_RECORD = "结束录制";
    private static final String PAUSE_RECORD = "暂停录制";
    private static final String RESUME_RECORD = "恢复录制";
    private PreviewView previewView;

    private Executor executor;

    private Preview preview;

    private ImageAnalysis imageAnalysis;

    private ImageCapture imageCapture;

    private VideoCapture<Recorder> videoCapture;

    private Recording videoRecording;

    private ProcessCameraProvider cameraProvider;

    private Camera camera;

    private boolean isRecordingStart = false;

    private boolean isRecordingPause = false;

    private int cameraIndex = 0;

    private Button flashButton;

    private Button pauseResumeVideoButton;

    private Button startShopVideoButton;

    private Button switchCameraButton;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(R.layout.activity_camerax_preview);
        PermissionUtils.checkPermission(this, Manifest.permission.CAMERA,
                Manifest.permission.RECORD_AUDIO,
                Manifest.permission.WRITE_EXTERNAL_STORAGE);
        previewView = findViewById(R.id.camerax_preview_view);
        executor = ContextCompat.getMainExecutor(this);
        initCamerax();
        initButton();
    }

    private void initCamerax() {
        // 可以将相机生命周期绑定到activity,从而免去打开、关闭相机的任务
        ListenableFuture<ProcessCameraProvider> cameraProviderListenableFuture = ProcessCameraProvider.getInstance(this);

        cameraProviderListenableFuture.addListener(() -> {
            try {
                cameraProvider = cameraProviderListenableFuture.get();

                // 预览
                preview = new Preview.Builder().build();
                preview.setSurfaceProvider(previewView.getSurfaceProvider());

                // 图片分析
                imageAnalysis = new ImageAnalysis.Builder()
                        // 设置输出图片格式
                        .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)
                        // 非阻塞模式,只取最新的图片。若需要每帧图片都处理,使用阻塞模式 ImageAnalysis.STRATEGY_BLOCK_PRODUCER
                        .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                        .setTargetResolution(SIZE)
                        .build();
                imageAnalysis.setAnalyzer(executor, (imageProxy) -> {
                    // close后才认为处理完成当前帧,所以放到try-with-resources语句中
                    try (imageProxy) {
                        // 在此处处理图片,如对图片进行人脸、条码识别等
                        Log.i(TAG, "接收到一帧图片" + SIMPLE_DATE_FORMAT.format(new Date()));
                    } catch (Exception exception) {
                        Log.w(TAG, exception.getMessage());
                    }
                });

                // 拍照
                imageCapture = new ImageCapture.Builder()
                        .setFlashMode(ImageCapture.FLASH_MODE_AUTO)
                        .setTargetResolution(SIZE)
                        .build();

                // 录制
                Recorder recorder = new Recorder.Builder()
                        .setQualitySelector(QualitySelector.from(Quality.FHD))
                        .build();
                videoCapture = VideoCapture.withOutput(recorder);

                cameraProvider.unbindAll();
                camera = cameraProvider.bindToLifecycle(this, CAMERAS.get(cameraIndex), preview,
                        imageCapture, videoCapture);
            } catch (ExecutionException e) {
                throw new RuntimeException(e);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, executor);
    }

    private void initButton() {
        initFlashButton();
        initSwitchButton();
        initImageCaptureButton();
        initVideoCaptureButtons();
    }

    /**
     * 视频操作按钮
     */
    private void initVideoCaptureButtons() {
        startShopVideoButton = findViewById(R.id.camerax_video_capture_button);
        pauseResumeVideoButton = findViewById(R.id.camerax_video_capture_pause_button);

        // 开始/停止录制视频
        startShopVideoButton.setOnClickListener(view -> {
            if (videoCapture == null || executor == null) {
                return;
            }
            if (isRecordingStart) {
                videoRecording.stop();
                videoRecording = null;
                isRecordingStart = false;
                startShopVideoButton.setText(START_RECORD);
                // 视频录制停止后不显示"暂停/恢复录制"按钮
                pauseResumeVideoButton.setVisibility(View.GONE);
                // 录制视频完成后显示切换摄像头按钮
                switchCameraButton.setVisibility(View.VISIBLE);
                return;
            }
            PermissionUtils.checkPermission(this, Manifest.permission.RECORD_AUDIO);
            videoRecording = videoCapture.getOutput()
                    .prepareRecording(this, getMediaStoreOutputOptions())
                    .withAudioEnabled()
                    .start(executor, videoRecordEvent -> {
                    });
            isRecordingStart = true;
            startShopVideoButton.setText(STOP_RECORD);
            pauseResumeVideoButton.setText(PAUSE_RECORD);
            // 视频录制开始后显示"暂停/恢复录制"按钮
            pauseResumeVideoButton.setVisibility(View.VISIBLE);
            // 录制视频期间不允许切换摄像头,切换摄像头会终止录制
            switchCameraButton.setVisibility(View.GONE);
        });

        // 暂停/恢复录制视频
        pauseResumeVideoButton.setOnClickListener(view -> {
            if (videoCapture == null || executor == null || videoRecording == null || !isRecordingStart) {
                return;
            }
            if (isRecordingPause) {
                isRecordingPause = false;
                videoRecording.resume();
                pauseResumeVideoButton.setText(PAUSE_RECORD);
            } else {
                videoRecording.pause();
                isRecordingPause = true;
                pauseResumeVideoButton.setText(RESUME_RECORD);
            }
        });
    }

    /**
     * 拍照按钮
     */
    private void initImageCaptureButton() {
        findViewById(R.id.camerax_image_capture_button).setOnClickListener(view -> {
            if (imageCapture == null || executor == null) {
                return;
            }
            imageCapture.takePicture(getImageOutputFileOptions(), executor,
                    new ImageCapture.OnImageSavedCallback() {
                        @Override
                        public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
                            Log.i(TAG, "save picture success");
                            Toast.makeText(CameraxPreviewActivity.this, "success", Toast.LENGTH_SHORT).show();
                        }

                        @Override
                        public void onError(@NonNull ImageCaptureException exception) {
                            Log.w(TAG, "error" + exception.getMessage());
                            Toast.makeText(CameraxPreviewActivity.this, "fail", Toast.LENGTH_SHORT).show();
                        }
                    });
        });
    }

    /**
     * 切换摄像头按钮
     */
    private void initSwitchButton() {
        switchCameraButton = findViewById(R.id.camerax_switch_camera);
        switchCameraButton.setOnClickListener((view -> {
            if (cameraProvider == null) {
                return;
            }
            if (++cameraIndex >= CAMERAS.size()) {
                cameraIndex = 0;
            }
            cameraProvider.unbindAll();
            cameraProvider.bindToLifecycle(this, CAMERAS.get(cameraIndex), preview, imageAnalysis,
                    imageCapture, videoCapture);
            resetButtonStatus(cameraIndex);
        }));
    }

    /**
     * 闪光灯按钮
     */
    private void initFlashButton() {
        flashButton = findViewById(R.id.camerax_flash);
        flashButton.setOnClickListener(view -> {
            if (camera == null) {
                return;
            }
            boolean isTorchOff = camera.getCameraInfo().getTorchState().getValue() == TorchState.OFF;
            camera.getCameraControl().enableTorch(isTorchOff);
            flashButton.setText(camera.getCameraInfo().getTorchState().getValue() == TorchState.ON ? "关灯" : "开灯");
        });
    }

    /**
     * 重设按钮状态
     */
    private void resetButtonStatus(int cameraIndex) {
        flashButton.setText("开灯");
        flashButton.setVisibility(cameraIndex == 0 ? View.VISIBLE : View.GONE);
        startShopVideoButton.setText(START_RECORD);
        pauseResumeVideoButton.setText(PAUSE_RECORD);
        pauseResumeVideoButton.setVisibility(View.GONE);
    }

    private ImageCapture.OutputFileOptions getImageOutputFileOptions() {
        return new ImageCapture
                .OutputFileOptions.Builder(getContentResolver(), MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                getContentValues("image/png"))
                .build();
    }

    private MediaStoreOutputOptions getMediaStoreOutputOptions() {
        return new MediaStoreOutputOptions.Builder(getContentResolver(),
                MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
                .setContentValues(getContentValues("video/mp4"))
                .build();
    }

    private ContentValues getContentValues(String mimeType) {
        String name = SIMPLE_DATE_FORMAT.format(new Date());
        ContentValues contentValues = new ContentValues();
        contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
        contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
            contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/HeTools");
        }
        return contentValues;
    }
}

参考文章

  1. 开始使用Android相机

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

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

相关文章

基于Java Springboot高校工作室管理系统

一、作品包含 源码数据库设计文档万字PPT全套环境和工具资源部署教程 二、项目技术 前端技术&#xff1a;Html、Css、Js、Vue、Element-ui 数据库&#xff1a;MySQL 后端技术&#xff1a;Java、Spring Boot、MyBatis 三、运行环境 开发工具&#xff1a;IDEA/eclipse 数据…

React(二)

文章目录 项目地址七、数据流7.1 子组件传递数据给父组件7.1.1 方式一:給父设置回调函数,传递给子7.1.2 方式二:直接将父的setState传递给子7.2 给props传递jsx7.2.1 方式一:直接传递组件给子类7.2.2 方式二:传递函数给子组件7.3 props类型验证7.4 props的多层传递7.5 cla…

浅谈丨功能安全测试,汽车的守护者

随着新能源汽车迅猛的发展&#xff0c;各类车型频频面世&#xff0c;同时辅助驾驶/自动驾驶等智驾功能也在不断迭代&#xff0c;使得整个汽车系统的复杂性越来越高&#xff0c;最终导致消费者不得不对如今的汽车质量和安全性提出质疑。 如何打破质疑&#xff1f; 那就不得不搬…

bridge-multicast-igmpsnooping

# 1.topo # 2.创建命名空间 ip netns add ns0 ip netns add ns1 ip netns add ns2 ip netns add ns3 # 3.创建veth设备 ip link add ns0-veth0 type veth peer name hn0-veth0 ip link add ns1-veth0 type veth peer name hn1-veth0 ip link add ns2-veth0 type veth pe…

密码学11

概论 计算机安全的最核心三个关键目标&#xff08;指标&#xff09;/为&#xff1a;保密性 Confidentiality、完整性 Integrity、可用性 Availability &#xff0c;三者称为 CIA三元组 数据保密性&#xff1a;确保隐私或是秘密信息不向非授权者泄漏&#xff0c;也不被非授权者使…

MIT 6.S081 | 操作系统 | Lab1: Xv6 and Unix utilities

Lab1: Xv6 and Unix utilities 文章目录 Lab1: Xv6 and Unix utilities实验任务1.启动XV6(easy)2.Sleep(easy)-练手的&#xff0c;就是熟悉一下怎么在xv6项目中加.c文件&#xff0c;生成可执行程序并进行测试的1.解析rm.c2.argc 如何被赋值3.Sleep代码4.makefile编辑5.通过make…

在SpringBoot项目中集成MongoDB

文章目录 1. 准备工作2. 在SpringBoot项目中集成MongoDB2.1 引入依赖2.2 编写配置文件2.3 实体类 3. 测试4. 文档操作4.1 插入操作4.1.1 单次插入4.1.2 批量插入 4.2 查询操作4.2.1 根据id查询4.2.2 根据特定条件查询4.2.3 正则查询4.2.4 查询所有文档4.2.5 排序后返回 4.3 删除…

美团-Leaf ID算法集成到SpringBoot项目

提前准备 下载源码 GitHub地址&#xff1a;https://github.com/Meituan-Dianping/Leaf 下载下来 然后 maven install 安装到本地仓库 再需要用到该ID算法的项目中引入 以下内容 <!-- 本地仓库中的Leaf --> <dependency><artifactId>leaf-boot-starte…

AI+若依框架项目

基础应用篇 1.若依搭建 技术选型 RuoYi-Vue版本&#xff0c;采用了前后端分离的单体架构设计&#xff1a; 软件环境&#xff1a;JDK、MySQL 、Redis 、Maven、Node 技术选型&#xff1a;Spring Boot、Spring Security、MyBatis、Jwt、V 官方推荐 课程版本 JDK > 1.8 …

RabbitMQ高可用延迟消息惰性队列

目录 生产者确认 消息持久化 消费者确认 TTL延迟队列 TTL延迟消息 惰性队列 生产者确认 生产者确认就是&#xff1a;发送消息的人&#xff0c;要确保消息发送给了消息队列&#xff0c;分别是确保到了交换机&#xff0c;确保到了消息队列这两步。 1、在发送消息服务的ap…

将django+vue项目发布部署到服务器

1.部署django后端服务 部署架构 1.1 下载依赖插件 pip3.8 freeze > requirements.txt1.2 安装依赖插件 pip3 install -r requirements.txt1.3 安装mysql数据库 apt install mysql-server初始化数据库 CREATE USER admin% IDENTIFIED WITH mysql_native_password BY 123…

论文阅读:SIMBA: single-cell embedding along with features

Chen, H., Ryu, J., Vinyard, M.E. et al. SIMBA: single-cell embedding along with features. Nat Methods 21, 1003–1013 (2024). 论文地址&#xff1a;https://doi.org/10.1038/s41592-023-01899-8 代码地址&#xff1a;https://github.com/pinellolab/simba. 摘要 大多…

商业物联网:拥抱生产力的未来

在现代商业格局中&#xff0c;数据占据至高无上的地位。物联网&#xff08;IoT&#xff09;站在这场数字革命的前沿&#xff0c;将以往模糊不清的不确定因素转变为可衡量、可付诸行动的深刻见解。物联网技术为日常物品配备传感器与连接功能&#xff0c;使其能够实时收集并传输数…

【FRP 内网穿透 从0到1 那些注意事项】

【摘要】 最近跟第三方团队调试问题&#xff0c;遇到一个比较烦的操作。就是&#xff0c;你必须要发个版到公网环境&#xff0c;他们才能链接到你的接口地址&#xff0c;才能进行调试。按理说&#xff0c;也没啥&#xff0c;就是费点时间。但是&#xff0c;在调试的时候&#…

最新Kali安装详细版教程(附安装包,傻瓜式安装教程)

本文主要详细介绍 kali 的安装过程&#xff0c;以及安装完成后的基本设置&#xff0c;比如安装增强工具&#xff0c;安装中文输入法以及更新升级等操作。 文章目录 实验环境准备工作步骤说明安装虚拟机安装 Kali安装增强工具安装中文输入法更新升级 实验环境 VMware &#x…

【山大909算法题】2014-T1

文章目录 1.原题2.算法思想3.关键代码4.完整代码5.运行结果 1.原题 为带表头的单链表类Chain编写一个成员函数Reverse&#xff0c;该函数对链表进行逆序操作&#xff08;将链表中的结点按与原序相反的顺序连接&#xff09;&#xff0c;要求逆序操作就地进行&#xff0c;不分配…

论文浅尝 | MindMap:知识图谱提示激发大型语言模型中的思维图(ACL2024)

笔记整理&#xff1a;和东顺&#xff0c;天津大学硕士&#xff0c;研究方向为软件缺陷分析 论文链接&#xff1a;https://aclanthology.org/2024.acl-long.558/ 发表会议&#xff1a;ACL 2024 1. 动机 虽然大语言模型&#xff08;LLMs&#xff09;已经在自然语言理解和生成任务…

Win11 22H2/23H2系统11月可选更新KB5046732发布!

系统之家11月22日报道&#xff0c;微软针对Win11 22H2/23H2版本推送了2024年11月最新可选更新补丁KB5046732&#xff0c;更新后&#xff0c;系统版本号升至22621.4541和22631.4541。本次更新后系统托盘能够显示缩短的日期和时间&#xff0c;文件资源管理器窗口很小时搜索框被切…

SpringSecurity创建一个简单的自定义表单的认证应用

1、SpringSecurity 自定义表单 在 Spring Security 中创建自定义表单认证应用是一个常见的需求&#xff0c;特别是在需要自定义登录页面、认证逻辑或添加额外的表单字段时。以下是一个详细的步骤指南&#xff0c;帮助你创建一个自定义表单认证应用。 2、基于 SpringSecurity 的…

Cloud Native 云原生后端的开发注意事项

在云原生后端开发里&#xff0c;数据管理和存储这块得好好弄。数据库选型得综合考虑&#xff0c;像关系型数据有复杂查询需求就选 MySQL、PostgreSQL&#xff0c;海量非结构化数据就可以考虑 MongoDB、Cassandra 这些。设计数据库得遵循规范化原则&#xff0c;像设计电商订单表…