用一個專案把控制層、業務層、持久層說明白了,每一句話都講的很清楚

ivanlee717發表於2024-11-11

實現一個資料庫和前端的互動

三層結構

image-20241111170345140

  • 持久層開發:依據前端頁面的設定規劃相關的sql語句,以及進行配置

  • 業務層開發:核心功能控制、業務操作以及異常的處理

  • 控制層開發:前後端連線,接受請求,處理響應

完整的資料響應流程如下:

  1. 前端發起請求: 前端透過瀏覽器或其他客戶端發起HTTP請求,請求後端提供的某個API介面或頁面資源。請求可以包括HTTP方法(GET、POST、PUT、DELETE等)、URL、請求頭和請求體等資訊。

  2. 請求到達伺服器: 請求透過網路到達伺服器。伺服器會根據請求的URL和HTTP方法來決定路由到哪個Controller處理。

  3. DispatcherServlet處理: Spring Boot中使用了DispatcherServlet作為前端控制器,它會接收到所有的HTTP請求,然後將請求分發給合適的Controller。

  4. Controller處理: 根據請求的URL和HTTP方法,DispatcherServlet將請求轉發給對應的Controller處理。Controller是Spring MVC中的元件,負責處理具體的請求並生成響應。

  5. 業務邏輯處理: 在Controller中,可以進行業務邏輯的處理,包括呼叫Service層的方法、讀取資料庫、計算等。

  6. 呼叫Service層: 通常,業務邏輯的處理會涉及呼叫Service層的方法。Service層負責業務邏輯的組織和封裝。

  7. 訪問資料庫或其他資源: 在Service層,可能需要訪問資料庫或其他外部資源來獲取資料或執行一些操作。

  8. 返回響應資料: 處理完業務邏輯後,Controller會生成響應資料,通常是一個檢視模板、JSON資料或其他格式的資料。

  9. 響應返回到前端: Controller生成的響應資料會透過HTTP協議返回給前端,響應包括HTTP狀態碼、響應頭和響應體等資訊。

接下來就進行一個完整的後端的三層框架的應用。

基礎配置

根據以下過程初始化一個springBoot框架,這個其實也可以用Maven進行建立,但是Spring Initializr會幫助我們建立好一些測試檔案和依賴。

下圖是一般我們在建立專案時都會選擇通用一點的JDK8版本,我用的spring boot版本是2.4左右。如果版本都太新的話,可能會發生很多莫名其妙的不相容問題。但是預設的伺服器好像已經沒有8了,我們按照以下步驟來修改。

image-20241102100948436

image-20241102101043721

image-20241102101208269

application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/store?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=12345

配置到這一步的話,再配合我[上一篇的配置資料庫的過程](小白手把手教學用spring框架實現mybatis和mysql以及工作原理 - ivanlee717 - 部落格園),我們就完成了初步的配置。上述檔案放在src/main/resources/application.properties檔案裡面,然後我們進行單元測試。

單元測試

D:.
├─.idea
│  ├─dataSources
│  ├─inspectionProfiles
│  └─libraries
├─src
│  ├─main
│  │  ├─java
│  │  │  └─com
│  │  │      └─ivan
│  │  │          └─store
│  │  └─resources
│  │      ├─mapper
│  └─test
│      └─java
│          └─com
│              └─ivan
│                  └─store
│                      ├─mapper
│                      └─service


這是初始的一個目錄結構,test目錄裡有完全一樣的結構,就是為了方便我們在完成任何一項功能的測試。以下是我們的測試程式碼。

# com/ivan/store/StoreApplicationTests.java

package com.ivan.store;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.sql.DataSource;
import java.sql.SQLDataException;
import java.sql.SQLException;

@SpringBootTest
class StoreApplicationTests {
    @Autowired //自動裝配
    private DataSource dataSource;
    @Test
    void contextLoads() {
    }
    @Test # 測試連線資料庫
    void getConnection() throws SQLException {
        System.out.println(dataSource.getConnection());
    }

}  

@Test就是表明我們只執行該函式來測試我們的資料庫連通性。

image-20241102104821732

/**
 * 資料庫連線池
 * 1. DBCP
 * 2. Spring MVC: C3P0
 * 3. Hikari 預設內部連線池
 * HikariProxyConnection@2127123542 預設內部連線池
 * wrapping com.mysql.cj.jdbc.ConnectionImpl@748a654a
 */

idea對於Js程式碼的相容姓比較,所以有時候不能夠正常的載入。方法:清楚idea快取,或者maven的clean,install,

rebuild專案。

image-20241102105835992

到此為止我們初步的配置就都得到了測試透過,接下里進行必要的類建立,

建立類

entity實體構造

首先我們建立一個entity的軟體包,這裡相當於是和model功能一致的資料庫,只不過用於存放我們的實體類,與資料庫中的屬性值基本保持一致,實現set和get的方法。我們先透過sql語句建立一個資料庫,具體的欄位如下

image-20241111172139399

create table t_user(
    uid int AUTO_INCREMENT comment '使用者id',
    username varchar(20) not null unique comment '使用者名稱',
    password char(32) not null comment '密碼',
    salt char(36) comment '鹽值',
    phone varchar(20) comment '電話號碼',
    email varchar(30)   comment '電子郵箱',
    gender int comment '0女1男',
    avatar varchar(50) comment '頭像',
    is_delete int comment '0未刪除,1已刪除',
    create_user varchar(20) comment '日誌建立人',
    create_time DATETIME comment '建立時間',
    modified_user varchar(20) comment '修改執行人',
    modified_time DATETIME comment '修改時間',
    primary key (uid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

image-20241102154116436

注意在建立時間時,我們用到的類的庫時util裡面的。

因為我們有很多個表裡面的欄位是完全一樣的,所以如果每個類都寫一遍相同的程式碼很不方便,於是透過表的結構提取出表的公共欄位,放在一個實體類的基類裡面BaseEntity,然後再用其他的類來繼承這個類。

BaseEntity.java
package com.ivan.store.entity;

import java.io.Serializable;
import java.util.Date;
import java.util.Objects;

//作為實體類的基類,需要滿足一些約束/為了便於資料傳輸,實現序列化介面
//@Data
public class BaseEntity implements Serializable {
    private String createUser;
    private Date createTime;
    private String modifiedUser;
    private Date modifiedTime;

這四個欄位就是我們公共的表資訊,但是細心的人會發現資料庫裡的命名方式是create_user,而類中定義的是createUser這種寫法,這是一種開發規範,我們在後續持久層程式碼中會著重關注這一點。

public Date getCreateTime() {
        return createTime;
    }

    public String getCreateUser() {
        return createUser;
    }

    public Date getModifiedTime() {
        return modifiedTime;
    }

    public String getModifiedUser() {
        return modifiedUser;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    public void setCreateUser(String createUser) {
        this.createUser = createUser;
    }

    public void setModifiedTime(Date modifiedTime) {
        this.modifiedTime = modifiedTime;
    }

    public void setModifiedUser(String modifiedUser) {
        this.modifiedUser = modifiedUser;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof BaseEntity)) return false;
        BaseEntity that = (BaseEntity) o;
        return Objects.equals(getCreateUser(), that.getCreateUser()) && Objects.equals(getCreateTime(), that.getCreateTime()) && Objects.equals(getModifiedUser(), that.getModifiedUser()) && Objects.equals(getModifiedTime(), that.getModifiedTime());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getCreateUser(), getCreateTime(), getModifiedUser(), getModifiedTime());
    }

    @Override
    public String toString() {
        return "BaseEntity{" +
                "createUser='" + createUser + '\'' +
                ", createTime=" + createTime +
                ", modifiedUser='" + modifiedUser + '\'' +
                ", modifiedTime=" + modifiedTime +
                '}';
    }
}

這一段話並不需要我們自己寫,在idea中按下快捷鍵alt+insert,選中我們的表欄位就可以自動生成對應的方法,至於為什麼一定要寫這些東西,可以參見[這篇文章](面試官:重寫 equals 時為什麼一定要重寫 hashCode?-騰訊雲開發者社群-騰訊雲)

image-20241111172817274

並且所有的實體類都需要進行方法重寫。

現在我們再編寫使用者的實體類

User
package com.ivan.store.entity;

import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.Date;
import java.util.Objects;

//@Component 自動的物件建立和修飾
public class User extends BaseEntity implements Serializable {
    private Integer uid;
    private String username;
    private String password;
    private String salt;
    private String phone;
    private String email;
    private Integer gender;
    private String avatar;
    private Integer isDelete;
    //get和set方法,equal方法和hashcode,tostring方法

    //alt+insert批次插入
    public Integer getUid() {
        return uid;
    }

    public void setUid(Integer uid) {
        this.uid = uid;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getSalt() {
        return salt;
    }

    public void setSalt(String salt) {
        this.salt = salt;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Integer getGender() {
        return gender;
    }

    public void setGender(Integer gender) {
        this.gender = gender;
    }

    public String getAvatar() {
        return avatar;
    }

    public void setAvatar(String avatar) {
        this.avatar = avatar;
    }

    public Integer getIsDelete() {
        return isDelete;
    }

    public void setIsDelete(Integer isDelete) {
        this.isDelete = isDelete;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        if (!super.equals(o)) return false;
        User user = (User) o;
        return Objects.equals(getUid(), user.getUid()) && Objects.equals(getUsername(), user.getUsername()) && Objects.equals(getPassword(), user.getPassword()) && Objects.equals(getSalt(), user.getSalt()) && Objects.equals(getPhone(), user.getPhone()) && Objects.equals(getEmail(), user.getEmail()) && Objects.equals(getGender(), user.getGender()) && Objects.equals(getAvatar(), user.getAvatar()) && Objects.equals(getIsDelete(), user.getIsDelete());
    }

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), getUid(), getUsername(), getPassword(), getSalt(), getPhone(), getEmail(), getGender(), getAvatar(), getIsDelete());
    }

    @Override
    public String toString() {
        return "User{" +
                "uid=" + uid +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", salt='" + salt + '\'' +
                ", phone='" + phone + '\'' +
                ", email='" + email + '\'' +
                ", gender=" + gender +
                ", avatar='" + avatar + '\'' +
                ", isDelete=" + isDelete +
                '}';
    }
}

持久層

所謂的持久層就是把資料可以永久保持的儲存到裝置中,不像放到記憶體中那樣斷電就消失,一般來說,持久層為直接的理解就是對資料庫的各種操作,如CRUD(增加,刪除,修改,查詢),更新等操作

持久層,就是把持久的動作封裝成一個獨立的層,這是為了降低功能程式碼之間的關聯.建立一個更清晰的抽象,提高程式碼的內聚力,降低程式碼的耦合度,提高可維護性和複用性.

image-20241111173823885

具體到程式碼開發,就是規劃需要的sql語句,mybatis運算元據庫insert into t_user(username,password) value(),使用者註冊時要去查詢當前的使用者名稱是否存在,如果存在不能註冊select * from t_user where username = ?

此時我們就需要用到mapper這個概念,這個東西涉及到了對映image-20241111174835797

具體功能就是從我們的實體類裡面獲取到資料資訊,然後編寫一些介面功能,這些功能又可以服務於service層裡的具體實現函式。程式碼如下:

建立mapper目錄,建立不同的介面UserMapper,注意這裡選擇的是介面

image-20241102163248012

我們知道,有抽象方法的類被稱為抽象類,也就意味著抽象類中還能有不是抽象方法的方法。這樣的類就不能算作純粹的介面,儘管它也可以提供介面的功能——只能說抽象類是普通類與介面之間的一種中庸之道。

語法層面上

  • 抽象類可以提供成員方法的實現細節,而介面中只能存在 public abstract 方法;
  • 抽象類中的成員變數可以是各種型別的,而介面中的成員變數只能是 public static final 型別的;
  • 介面中不能含有靜態程式碼塊,而抽象類可以有靜態程式碼塊;
  • 一個類只能繼承一個抽象類,而一個類卻可以實現多個介面。

介面定義sql語句的抽象方法,啟動類定義介面註解

package com.ivan.store.mapper;

import com.ivan.store.entity.User;
import org.apache.ibatis.annotations.Mapper;

/**
 * 使用者模組的持久層介面
 */
//@Mapper 缺陷:一個專案有很多個mapper,在啟動類裡新增註解MapperScan
public interface UserMapper {
    /**
     * 插入使用者的資料
     * @param user 使用者的資料
     * @return 受影響的行數(增刪改查影響的行數作為返回值,判斷是否執行成功)
     */
    Integer insert(User user);

    /**
     * 根據使用者名稱查詢使用者的資料
     * @param username 使用者名稱
     * @return 找到則返回使用者資料,沒有找到返回null
     */
    User findByUsername(String username);
}

//@Mapper 缺陷:一個專案有很多個mapper,在啟動類裡新增註解MapperScan這裡的註釋意思是每一個介面檔案都要新增這個註解,不如在啟動類中直接新增一個MapperScan註解,在啟動時就會自動掃描所有的對映然後放到spring裡面。image-20241111175739152

現在編寫好介面類之後,具體應該怎麼實現呢

答:編寫對映,定義xml對映檔案和對應的介面進行關聯。屬於資原始檔,要放到resource目錄下,對應的對映檔案和介面名字保持一致

mapper.xml

src/main/resources/mapper/UserMapper.xml程式碼如下,我們依次來解析這段xml語言

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">

這是程式碼固定的開頭,之後我們要編寫mapper標籤裡具體的內容。

<mapper namespace="com.ivan.store.mapper.UserMapper">namespace名稱空間是要告訴spring去哪裡找到對應的介面,需要一個完整的路徑。在這個大標籤下,我們對兩個介面函式進行編寫。

<resultMap id="UserEntityMap" type="com.ivan.store.entity.User">這個resultMap標籤是自定義對映規則標籤必須帶的,核心屬性id表示分配一個id值,對應的是resultMap屬性的值,type表示對查詢結果與java的哪一個類的對映。之前因為資料庫欄位和我們類中定義的規範不一樣,我們就需要在這裡進行一個對映

<resultMap id="UserEntityMap" type="com.ivan.store.entity.User">
        <!--將表的欄位和類的屬性不一致的欄位進行匹配指定
        column是表的欄位,property是對映名稱-->
        <id column="uid" property="uid"></id><!--在定義對映規則時,主鍵不可以省略-->
        <result column="is_delete" property="isDelete"></result>
        <result column="create_user" property="createUser"></result>
        <result column="create_time" property="createTime"></result>
        <result column="modified_user" property="modifiedUser"></result>
        <result column="modified_time" property="modifiedTime"></result>
    </resultMap>

然後將規則統一之後,我們就可以根據不同的規則來進行sql語句的編寫

<insert id="insert" useGeneratedKeys="true" keyProperty="uid">
        INSERT INTO t_user(
        username ,password,salt, phone, email, gender,
        avatar, is_delete, create_user, create_time, modified_user,
        modified_time)
        VALUES
        ( #{username} ,#{password},#{salt}, #{phone}, #{email}, #{gender},
        #{avatar}, #{isDelete}, #{createUser}, #{createTime}, #{modifiedUser},
        #{modifiedTime})
    </insert>

id屬性:表示對映的介面方法的名稱,直接在標籤的內容來編寫sql語句-->
    <!--useGeneratedKeys="true": uid主鍵自增 keyProperty="uid":uid作為主鍵

select在執行的時候,查詢的結果是一個物件或者多個物件
單個物件:resultType:表示查詢的結果及型別,需要指定對應的對映類的型別,並且包含完整的包介面
resultMap:表的欄位和類的物件屬性欄位不一致時,來自定義查詢結果集的對映規則,-->

查詢語句

<select id="findByUsername" resultMap="UserEntityMap">
        SELECT * FROM t_user WHERE username = #{username}
    </select>  這裡就用到了前面定義規則的id

單元測試

每個獨立的層要編寫單元測試方法,測試每層的程式碼範圍,在test包結構下創一個mapper包,在這個包下面再建立持久層的測試

runwith知識點

@runWith註解作用:

  • @RunWith就是一個執行器
  • @RunWith(JUnit4.class)就是指用JUnit4來執行
  • @RunWith(SpringJUnit4ClassRunner.class),讓測試執行於Spring測試環 境,以便在測試開始的時候自動建立Spring的應用上下文
  • @RunWith(Suite.class)的話就是一套測試集合
package com.ivan.store.mapper;


import com.ivan.store.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@SpringBootTest // 表示標註當前的類是一個測試類,不會隨同專案一塊打包
@RunWith(SpringRunner.class)//表示啟動這個單元測試類(獨立的單元測試不能執行),引數必須是SpringRunner的例項型別
public class UserMapperTests {
    // idea有檢測的功能,介面是不能直接建立bean的,用動態代理解決
    @Autowired
    private UserMapper userMapper;
    /**
     * 單元測試方法:單獨執行,不用啟動整個專案,可以做單元測試
     * 1. 必須被Test註解修飾
     * 2. 返回值型別必須是void
     * 3. 方法的引數列表不能指定任何型別
     * 4. 方法的訪問修飾符必須是public
     */
    @Test
    public void insert(){
        User user = new User();
        user.setUsername("ivan");
        user.setPassword("regina");
        Integer rows = userMapper.insert(user);
        System.out.println(rows);
    }
    @Test
    public void findByUsername(){
        User user = userMapper.findByUsername("ivan");
        System.out.println(user);
    }
}

idea有檢測的功能,介面是不能直接建立bean的,用動態代理解決,否則會讓userMapper裡面的函式變成static狀態,這樣的話又無法被呼叫。所以下列給出了修改以及執行測試的過程。

image-20241105144333368

image-20241105144643977

image-20241105163745734

image-20241105164917058

可以看到這個地方輸出了資料庫資訊,說明持久層寫好了

業務層

規劃異常

實現業務的主要邏輯,是系統架構中體現核心價值的部分。將一個業務中所有的操作封裝成一個方法,同時保證方法中所有的資料庫更新操作,即保證同時成功或同時失敗。避免部分成功部分失敗引起的資料混亂操作。在完成業務邏輯實現之前要先規劃異常,比如使用者在進行註冊的時候可能產生使用者名稱已存在,丟擲一個異常

RuntimeException異常是一般異常,作為這種異常的子類,然後再去定義具體的異常來繼承。serviceException繼承作為RuntimeException的基類去做擴充。

以下方法實現了對基類異常的重寫

image-20241105172933433

package com.ivan.store.service.ex;

/**業務異常的基類**/
public class ServiceException extends  RuntimeException{
    //重寫
    public ServiceException() {
        super(); //只想拋一個異常 throw new ServiceException()
    }

    public ServiceException(String message) {
        super(message); //throw new ServiceException("業務層產生未知的異常")
    }

    public ServiceException(String message, Throwable cause) {
        super(message, cause); 
    }

    public ServiceException(Throwable cause) {
        super(cause);
    }

    protected ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

根據業務層不同的功能來詳細定義具體的異常的型別,統一去繼承serviceException

註冊時的異常:使用者名稱已存在UsernameDepulicatedExpecption

未知的異常:執行資料插入操作的時候,伺服器或者資料庫當機。處於正在執行插入的過程中所產生的異常:InsertException

設計介面:抽象方法和定義,在service包下建立一個以I開頭的名字,IUserService

介面和impl實現

建立一個實現IUserService介面的類UserServiceImpl的實現方法

image-20241111201919727

同樣的我們定義IUserService是我們的介面,然後建立一個新的軟體包impl包來對所有的介面進行實現。

package com.ivan.store.service;

import com.ivan.store.entity.User;

/**用於模組業務層介面**/
public interface IUserService {
    /**
     * 使用者註冊方法
     */
    void reg(User user);
}

package com.ivan.store.service.impl;

import com.ivan.store.entity.User;
import com.ivan.store.mapper.UserMapper;
import com.ivan.store.service.IUserService;
import com.ivan.store.service.ex.InsertException;
import com.ivan.store.service.ex.UsernameDuplicatedException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;

import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Locale;
import java.util.UUID;

/**使用者模組業務層的實現類**/
@Service//不新增這個會報錯
/**
 * @Service:將當前類的物件交給Spring管理,自動建立物件以及物件的維護
 */
public class UserServiceImpl implements IUserService {
    @Autowired
    //private User user;
    private UserMapper userMapper;
    /**呼叫mapper層的方法**/
    @Override
    public void reg(User user) {
        //透過User引數獲取傳遞過來的username
        String username = user.getUsername();
        //呼叫mapper的findByUsername方法判斷是否使用者已存在
        User result = userMapper.findByUsername(username);
        if (result!=null){
            //丟擲異常
            throw new UsernameDuplicatedException("使用者名稱被佔用");
        }

        /**
         * 資料補全:
         * is_delete->0,
         * 建立時間
         */
        user.setIsDelete(0);
        user.setCreateUser(user.getUsername());
        user.setModifiedUser(user.getUsername());
        Date date = new Date();
        user.setCreateTime(date);
        user.setModifiedTime(date);


        // 執行註冊業務功能
        Integer rows = userMapper.insert(user);
        if(rows != 1){
            throw new InsertException("註冊過程出錯");
        }
    }

單元測試

在單元測試包下建立UserServiceTest類測試與上述功能

image-20241111103641395

ctrl+alt+t自動加一個異常捕獲

image-20241111104024620

這個需要新增Service註解,在IUserServiceImpl.java裡面新增註解表示這是service層的功能,不可以缺失

image-20241111104242458

package com.ivan.store.service;


import com.ivan.store.entity.User;
import com.ivan.store.mapper.UserMapper;
import com.ivan.store.service.ex.ServiceException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.stereotype.Service;
import org.springframework.test.context.junit4.SpringRunner;

@SpringBootTest // 表示標註當前的類是一個測試類,不會隨同專案一塊打包
@RunWith(SpringRunner.class)//表示啟動這個單元測試類(獨立的單元測試不能執行),引數必須是SpringRunner的例項型別
public class UserServiceTests {
    // idea有檢測的功能,介面是不能直接建立bean的,用動態代理解決
    @Autowired
    private IUserService userService;
    /**
     * 單元測試方法:單獨執行,不用啟動整個專案,可以做單元測試
     * 1. 必須被Test註解修飾
     * 2. 返回值型別必須是void
     * 3. 方法的引數列表不能指定任何型別
     * 4. 方法的訪問修飾符必須是public
     */
    @Test
    public void reg(){
        try {
            User user = new User();
            user.setUsername("regina1");
            user.setPassword("ivan");
            userService.reg(user);
            System.out.println("test ok");
        } catch (ServiceException e) {
            //獲取類的物件,再獲取類的名稱
            System.out.println(e.getClass().getSimpleName());
            //異常的具體描述資訊
            System.out.println(e.getMessage());
        }
    }

}

image-20241111104609081

salt加密

這一步就是為了實現一個密碼的安全性,具體不做太多講解。

image-20241111105641567

//UserServiceImpl.java
private String getMd5Pwd(String pwd, String salt){
        return DigestUtils.md5DigestAsHex((salt+pwd+salt).getBytes(StandardCharsets.UTF_8)).toUpperCase();

    }
public void reg(User user) {
        //透過User引數獲取傳遞過來的username
        String username = user.getUsername();
        //呼叫mapper的findByUsername方法判斷是否使用者已存在
        User result = userMapper.findByUsername(username);
        if (result!=null){
            //丟擲異常
            throw new UsernameDuplicatedException("使用者名稱被佔用");
        }
        //密碼加密處理的實現 (Test之後補充)
        //salt + pwd + salt
        String pwd = user.getPassword();
        String salt = UUID.randomUUID().toString().toUpperCase();
        String newpwd = getMd5Pwd(salt,pwd);
        user.setPassword(newpwd);
        user.setSalt(salt);

image-20241111112606714

控制層

建立響應

狀態碼、狀態描述資訊、資料。這部分功能封裝在一個類中,這個類作為方法的返回值返回給瀏覽器,我們統一設定在utils.JsonResult類裡面。

image-20241111142144167

package com.ivan.store.util;

import java.io.Serializable;

/**
 * Json格式的資料相應
 */
public class JsonResult<E> implements Serializable {
    private Integer state;//狀態碼
    private String message; //描述資訊
    private E data; //用E表示任何型別的資料型別

然後要構造一些相應的的建構函式,不同的引數可以接受不同的響應資料

    public JsonResult(){

    }
    public JsonResult(Integer state) {
        this.state = state;
    }

    public JsonResult(Integer state, E data) {
        this.state = state;
        this.data = data;
    }
    public JsonResult(Integer state, String msg, E data) {
        this.state = state;
        this.message = msg;
        this.data = data;
    }
    public JsonResult(Throwable e){
        this.message = e.getMessage();
    }

    public Integer getState() {
        return state;
    }

    public void setState(Integer state) {
        this.state = state;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public E getData() {
        return data;
    }

    public void setData(E data) {
        this.data = data;
    }
}

向後端伺服器傳送請求:依據當前的業務功能模組進行設計如下。

請求路徑:/users/reg
請求引數: User user
請求型別:POST/GET
響應資料:JsonResult<void>

處理請求

  1. 建立一個控制層對應的類UserController依賴於業務層的介面。
//@Controller
@RestController //@RestController = //@Controller + @ResponseBody
@RequestMapping("/users")
public class UserController {
    @Autowired
    private IUserService userService;

    //@ResponseBody // 表示此方法的響應結果以json格式進行資料響應
    @RequestMapping("/reg")
    public JsonResult<Void> reg(User user) {
        JsonResult<Void> res = new JsonResult<>();
        try {
            userService.reg(user);
            res.setState(200);
            res.setMessage("使用者註冊成功");
        } catch (UsernameDuplicatedException e) {
            res.setState(4000);
            res.setMessage("使用者名稱被佔用");
        } catch (InsertException e) {
            res.setState(5000);
            res.setMessage("註冊時產生未知的異常");

        }
        return res;
    }
}

Void 是 Java 中的一個特殊型別,它實際上並不表示任何具體的值。

泛型介紹

在 Java 中,泛型允許你在類、介面和方法中使用型別引數,從而使得這些類、介面和方法可以處理多種不同型別的物件,同時提供編譯時型別檢查。

JsonResult
假設 JsonResult 是一個泛型類,定義如下:

public class JsonResult<T> {
 private int code;
 private String message;
 private T data;
public JsonResult(T data) {
 this.data = data;
}

// 其他建構函式、getter和setter方法

}
在這個類定義中,T 是一個型別引數,表示 data 欄位的具體型別可以在例項化時指定。

例項化 JsonResult
當你建立一個 JsonResult 物件時,T 被具體化為 Void 型別。這意味著 data 欄位將不會包含任何實際的資料,因為它被設定為 Void 型別。

Exception e = new Exception("An error occurred");
JsonResult<Void> res = new JsonResult<>(e);

解釋
泛型引數 Void:
JsonResult 表示 JsonResult 的 data 欄位將不會包含任何實際的資料,因為 Void 型別不能例項化任何物件。
這通常用於表示某些方法或操作沒有具體的返回值,但仍然需要返回一個包含其他資訊(如狀態碼和訊息)的響應物件。
儘管 JsonResult 的建構函式接受一個 T 型別的引數,但在 JsonResult 中,T 被具體化為 Void。
因此,傳遞一個 Exception 物件 e 給建構函式看起來有些奇怪,但實際上,e 可能會被用於初始化 JsonResult 的其他欄位(如 message 或 code),而不是 data 欄位。
示例實現
為了更清楚地理解這一點,我們可以看一下 JsonResult 類的一個完整實現示例:
泛型引數 Void:
JsonResult 表示 JsonResult 的 data 欄位將不會包含任何實際的資料,因為 Void 型別不能例項化任何物件。
這通常用於表示某些方法或操作沒有具體的返回值,但仍然需要返回一個包含其他資訊(如狀態碼和訊息)的響應物件。
建構函式引數 e:
儘管 JsonResult 的建構函式接受一個 T 型別的引數,但在 JsonResult 中,T 被具體化為 Void。
因此,傳遞一個 Exception 物件 e 給建構函式看起來有些奇怪,但實際上,e 可能會被用於初始化 JsonResult 的其他欄位(如 message 或 code),而不是 data 欄位。
示例實現
為了更清楚地理解這一點,我們可以看一下 JsonResult 類的一個完整實現示例:

public class JsonResult<T> {
 private int code;
 private String message;
 private T data;}

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

public JsonResult(int code, String message) {
 this(code, message, null);
}

public JsonResult(T data) {
 this(200, "OK", data);
}

// Getter and Setter methods
public int getCode() {
 return code;
}

public void setCode(int code) {
 this.code = code;
}

public String getMessage() {
 return message;
}

public void setMessage(String message) {
 this.message = message;
}

public T getData() {
 return data;
}

public void setData(T data) {
 this.data = data;
}

}
使用示例

Exception e = new Exception("An error occurred");
JsonResult<Void> res = new JsonResult<>(200, "OK", null);

// 或者使用單引數建構函式
JsonResult<Void> res2 = new JsonResult<>(null);
結論
透過 JsonResult<Void> res = new JsonResult<>(e); 這段程式碼,res 的型別是 JsonResult<Void>,這意味著 res 的 data 欄位將不會包含任何實際的資料。如果你希望傳遞異常資訊,可以透過其他欄位(如 message)來傳遞。

具體的資料形式如圖所示:

image-20241111203203040

執行之後前端可以接收到對應的資料資訊。

image-20241111144150267

最佳化:將控制層最佳化設計

在控制層抽離一個父類來統一處理關於異常的相關內容 BaseController,因為一個專案會有很多的controller,我們把公共異常設定為一個父類,然後再用子類來繼承。

package com.ivan.store.controller;

import com.ivan.store.service.ex.InsertException;
import com.ivan.store.service.ex.ServiceException;
import com.ivan.store.service.ex.UsernameDuplicatedException;
import com.ivan.store.util.JsonResult;
import org.springframework.web.bind.annotation.ExceptionHandler;

/**
 * 控制層類的基類
 */
public class BaseController {
    public static final int RegIsOK = 200; //操作成功

    /**
     * 請求處理方法,這個方法的返回值就是需要傳遞給前端的資料
     * 自動將異常物件傳遞給此方法的引數列表上
     * 當專案中產生了異常,會被統一攔截到此方法中,這個方法就是請求處理方法
     */

    @ExceptionHandler(ServiceException.class) // 使用者統一處理丟擲的異常
    public JsonResult<Void> handlerException(Throwable e){
        JsonResult<Void> res = new JsonResult<>(e);
        if (e instanceof UsernameDuplicatedException){
            res.setState(4000);
            res.setMessage("使用者名稱已被佔用");
        }
        else if (e instanceof InsertException){
            res.setState(5000);
            res.setMessage("註冊時產生未知錯誤");
        }
        return res;
    }
}
package com.ivan.store.controller;

import com.ivan.store.entity.User;
import com.ivan.store.service.IUserService;
import com.ivan.store.service.ex.InsertException;
import com.ivan.store.service.ex.UsernameDuplicatedException;
import com.ivan.store.util.JsonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

//@Controller
@RestController //@RestController = //@Controller + @ResponseBody
@RequestMapping("/users")
public class UserController extends BaseController{
    @Autowired
    private IUserService userService;


    //@ResponseBody // 表示此方法的響應結果以json格式進行資料響應
    @RequestMapping("/reg")
    public JsonResult<Void> reg(User user){
        userService.reg(user);
       return new JsonResult<>(RegIsOK,"使用者註冊成功",null);
    }
}

image-20241111145650954

image-20241111204705601效果是一樣的。

這樣的話就把三層框架的使用基本講清楚了,後續還會寫一些更復雜一點的東西。如果需要程式碼原始碼可以聯絡博主,有什麼問題盡請諮詢。

相關文章