实现一个数据库和前端的交互
三层结构
-
持久层开发:依据前端页面的设置规划相关的sql语句,以及进行配置
-
业务层开发:核心功能控制、业务操作以及异常的处理
-
控制层开发:前后端连接,接受请求,处理响应
完整的数据响应流程如下:
前端发起请求: 前端通过浏览器或其他客户端发起HTTP请求,请求后端提供的某个API接口或页面资源。请求可以包括HTTP方法(GET、POST、PUT、DELETE等)、URL、请求头和请求体等信息。
请求到达服务器: 请求通过网络到达服务器。服务器会根据请求的URL和HTTP方法来决定路由到哪个Controller处理。
DispatcherServlet处理: Spring Boot中使用了DispatcherServlet作为前端控制器,它会接收到所有的HTTP请求,然后将请求分发给合适的Controller。
Controller处理: 根据请求的URL和HTTP方法,DispatcherServlet将请求转发给对应的Controller处理。Controller是Spring MVC中的组件,负责处理具体的请求并生成响应。
业务逻辑处理: 在Controller中,可以进行业务逻辑的处理,包括调用Service层的方法、读取数据库、计算等。
调用Service层: 通常,业务逻辑的处理会涉及调用Service层的方法。Service层负责业务逻辑的组织和封装。
访问数据库或其他资源: 在Service层,可能需要访问数据库或其他外部资源来获取数据或执行一些操作。
返回响应数据: 处理完业务逻辑后,Controller会生成响应数据,通常是一个视图模板、JSON数据或其他格式的数据。
响应返回到前端: Controller生成的响应数据会通过HTTP协议返回给前端,响应包括HTTP状态码、响应头和响应体等信息。
接下来就进行一个完整的后端的三层框架的应用。
基础配置
根据以下过程初始化一个springBoot框架,这个其实也可以用Maven进行创建,但是Spring Initializr
会帮助我们创建好一些测试文件和依赖。
下图是一般我们在创建项目时都会选择通用一点的JDK8版本,我用的spring boot版本是2.4左右。如果版本都太新的话,可能会发生很多莫名其妙的不兼容问题。但是默认的服务器好像已经没有8了,我们按照以下步骤来修改。
application.propertiesspring.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.javapackage 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;@Testvoid contextLoads() {}@Test # 测试连接数据库void getConnection() throws SQLException {System.out.println(dataSource.getConnection());}}
@Test就是表明我们只运行该函数来测试我们的数据库连通性。
/*** 数据库连接池* 1. DBCP* 2. Spring MVC: C3P0* 3. Hikari 默认内部连接池* HikariProxyConnection@2127123542 默认内部连接池* wrapping com.mysql.cj.jdbc.ConnectionImpl@748a654a*/
idea对于Js代码的兼容姓比较,所以有时候不能够正常的加载。方法:清楚idea缓存,或者maven的clean,install,
rebuild项目。
到此为止我们初步的配置就都得到了测试通过,接下里进行必要的类创建,
创建类
entity实体构造
首先我们创建一个entity的软件包,这里相当于是和model功能一致的数据库,只不过用于存放我们的实体类,与数据库中的属性值基本保持一致,实现set和get的方法。我们先通过sql语句创建一个数据库,具体的字段如下
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;
注意在创建时间时,我们用到的类的库时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;}@Overridepublic 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());}@Overridepublic int hashCode() {return Objects.hash(getCreateUser(), getCreateTime(), getModifiedUser(), getModifiedTime());}@Overridepublic String toString() {return "BaseEntity{" +"createUser='" + createUser + '\'' +", createTime=" + createTime +", modifiedUser='" + modifiedUser + '\'' +", modifiedTime=" + modifiedTime +'}';}
}
这一段话并不需要我们自己写,在idea中按下快捷键alt+insert
,选中我们的表字段就可以自动生成对应的方法,至于为什么一定要写这些东西,可以参见[这篇文章](面试官:重写 equals 时为什么一定要重写 hashCode?-腾讯云开发者社区-腾讯云)
并且所有的实体类都需要进行方法重写。
现在我们再编写用户的实体类
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;}@Overridepublic 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());}@Overridepublic int hashCode() {return Objects.hash(super.hashCode(), getUid(), getUsername(), getPassword(), getSalt(), getPhone(), getEmail(), getGender(), getAvatar(), getIsDelete());}@Overridepublic String toString() {return "User{" +"uid=" + uid +", username='" + username + '\'' +", password='" + password + '\'' +", salt='" + salt + '\'' +", phone='" + phone + '\'' +", email='" + email + '\'' +", gender=" + gender +", avatar='" + avatar + '\'' +", isDelete=" + isDelete +'}';}
}
持久层
所谓的持久层就是把数据可以永久保持的存储到设备中,不像放到内存中那样断电就消失,一般来说,持久层为直接的理解就是对数据库的各种操作,如CRUD(增加,删除,修改,查询),更新等操作
持久层,就是把持久的动作封装成一个独立的层,这是为了降低功能代码之间的关联.创建一个更清晰的抽象,提高代码的内聚力,降低代码的耦合度,提高可维护性和复用性.
具体到代码开发,就是规划需要的sql语句,mybatis操作数据库insert into t_user(username,password) value()
,用户注册时要去查询当前的用户名是否存在,如果存在不能注册select * from t_user where username = ?
此时我们就需要用到mapper这个概念,这个东西涉及到了映射
具体功能就是从我们的实体类里面获取到数据信息,然后编写一些接口功能,这些功能又可以服务于service层里的具体实现函数。代码如下:
创建mapper目录,创建不同的接口UserMapper,注意这里选择的是接口
我们知道,有抽象方法的类被称为抽象类,也就意味着抽象类中还能有不是抽象方法的方法。这样的类就不能算作纯粹的接口,尽管它也可以提供接口的功能——只能说抽象类是普通类与接口之间的一种中庸之道。
语法层面上
- 抽象类可以提供成员方法的实现细节,而接口中只能存在 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里面。
现在编写好接口类之后,具体应该怎么实现呢
答:编写映射,定义xml映射文件和对应的接口进行关联。属于资源文件,要放到resource目录下,对应的映射文件和接口名字保持一致
mapper.xml
src/main/resources/mapper/UserMapper.xml
代码如下,我们依次来解析这段xml语言
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//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的,用动态代理解决@Autowiredprivate UserMapper userMapper;/*** 单元测试方法:单独运行,不用启动整个项目,可以做单元测试* 1. 必须被Test注解修饰* 2. 返回值类型必须是void* 3. 方法的参数列表不能指定任何类型* 4. 方法的访问修饰符必须是public*/@Testpublic void insert(){User user = new User();user.setUsername("ivan");user.setPassword("regina");Integer rows = userMapper.insert(user);System.out.println(rows);}@Testpublic void findByUsername(){User user = userMapper.findByUsername("ivan");System.out.println(user);}
}
idea有检测的功能,接口是不能直接创建bean的,用动态代理解决,否则会让userMapper里面的函数变成static状态,这样的话又无法被调用。所以下列给出了修改以及运行测试的过程。
可以看到这个地方输出了数据库信息,说明持久层写好了
业务层
规划异常
实现业务的主要逻辑,是系统架构中体现核心价值的部分。将一个业务中所有的操作封装成一个方法,同时保证方法中所有的数据库更新操作,即保证同时成功或同时失败。避免部分成功部分失败引起的数据混乱操作。在完成业务逻辑实现之前要先规划异常,比如用户在进行注册的时候可能产生用户名已存在,抛出一个异常
RuntimeException
异常是一般异常,作为这种异常的子类,然后再去定义具体的异常来继承。serviceException继承作为RuntimeException
的基类去做拓展。
以下方法实现了对基类异常的重写
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
的实现方法
同样的我们定义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层的方法**/@Overridepublic void reg(User user) {//通过User参数获取传递过来的usernameString 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
类测试与上述功能
ctrl+alt+t
自动加一个异常捕获
这个需要添加Service注解,在IUserServiceImpl.java
里面添加注解表示这是service层的功能,不可以缺失
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的,用动态代理解决@Autowiredprivate IUserService userService;/*** 单元测试方法:单独运行,不用启动整个项目,可以做单元测试* 1. 必须被Test注解修饰* 2. 返回值类型必须是void* 3. 方法的参数列表不能指定任何类型* 4. 方法的访问修饰符必须是public*/@Testpublic 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());}}}
salt加密
这一步就是为了实现一个密码的安全性,具体不做太多讲解。
//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参数获取传递过来的usernameString username = user.getUsername();//调用mapper的findByUsername方法判断是否用户已存在User result = userMapper.findByUsername(username);if (result!=null){//抛出异常throw new UsernameDuplicatedException("用户名被占用");}//密码加密处理的实现 (Test之后补充)//salt + pwd + saltString pwd = user.getPassword();String salt = UUID.randomUUID().toString().toUpperCase();String newpwd = getMd5Pwd(salt,pwd);user.setPassword(newpwd);user.setSalt(salt);
控制层
创建响应
状态码、状态描述信息、数据。这部分功能封装在一个类中,这个类作为方法的返回值返回给浏览器,我们统一设置在utils.JsonResult
类里面。
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>
处理请求
- 创建一个控制层对应的类
UserController
依赖于业务层的接口。
//@Controller
@RestController //@RestController = //@Controller + @ResponseBody
@RequestMapping("/users")
public class UserController {@Autowiredprivate 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)来传递。
具体的数据形式如图所示:
运行之后前端可以接收到对应的数据信息。
优化:将控制层优化设计
在控制层抽离一个父类来统一处理关于异常的相关内容 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{@Autowiredprivate IUserService userService;//@ResponseBody // 表示此方法的响应结果以json格式进行数据响应@RequestMapping("/reg")public JsonResult<Void> reg(User user){userService.reg(user);return new JsonResult<>(RegIsOK,"用户注册成功",null);}
}
效果是一样的。
这样的话就把三层框架的使用基本讲清楚了,后续还会写一些更复杂一点的东西。如果需要代码源码可以联系博主,有什么问题尽请咨询。