用一个项目把控制层、业务层、持久层说明白了,每一句话都讲的很清楚

实现一个数据库和前端的交互

三层结构

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效果是一样的。

这样的话就把三层框架的使用基本讲清楚了,后续还会写一些更复杂一点的东西。如果需要代码源码可以联系博主,有什么问题尽请咨询。

posted @ 2024-11-11 20:53  ivanlee717  阅读(118)  评论(0编辑  收藏  举报