10天手敲一个SpringBoot网上商城项目(一)——项目环境搭建及用户注册功能的实现

静态资源及sql文件分享
链接:https://pan.baidu.com/s/1X-yjmQcPD3PqS21x0HplNA?pwd=23gr
提取码:23gr

项目环境搭建

1.项目分析

  1. 项目功能:登录,注册,热销商品,用户管理(密码,个人信息,头像,收货地址),购物车(展示,增加,删除),订单模块

  2. 开发顺序:注册,登录,用户管理,购物车,商品,订单模块

  3. 某一个模块的开发顺序:

    • 持久层开发:依据前端页面的设置规划相关的SQL语句,以及进行配置

    • 业务层开发:核心功能控制,业务操作以及异常的处理

    • 控制层开发:接收请求,处理响应

    • 前端开发:JS,Query,AJAX这些技术来连接后台

2.项目基本环境

  1. JDK:1.8版本及以上
  2. maven:需要配置到idea,3.6.1版本及以上
  3. 数据库:MariaDB,MySQL,要求是5.1版本及以上
  4. 开发的平台:idea开发

  1. 项目名称:store,表示商城
  2. 结构:com.cy.store
  3. 资源文件:resources文件夹下(static,templates)
  4. 单元测试:test.com.cy.store

3.项目创建

  1. Create New Project->

  2. 选择Spring Initializr,点击next

  3. 跳转到Project Metadata页面,该页面的Group填写域com和自己起的域名cy(即com.cy)==;Artifact填写项目名store;==Java Version版本选择自己安装的版本,点击next

  4. 选择需要导入的jar包:

    • 前后端的连接jar包:Web目录下的Spring web
    • mybatis的jar包:SQL目录下的Mybatis Framework
    • mysql数据库的驱动:SQL目录下的MySQL Driver
  5. 创建一个数据库

    create database store character set utf8;
    
  6. 在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=root
    

4.项目测试

4.1测试能否成功连接数据库

  • 启动Springboot主类,看idea中是否有对应的spring图形输出

  • 若idea有对应的spring图形输出开始第二步测试:在单元测试中测试数据库的连接是否可以加载:

    @SpringBootTest
    class StoreApplicationTests {
          
          
    
        @Autowired //自动装配
        private DataSource dataSource;
    
        @Test
        void contextLoads() {
          
          
        }
    
        @Test
        void getConnection() throws SQLException {
          
          
            System.out.println(dataSource.getConnection());
        }
    }
    

    运行getConnection方法,若成功返回HikariProxyConnection@189194499 wrapping com.mysql.cj.jdbc.ConnectionImpl@2b0e9f30则说明成功连接数据库,其中Hikari是一个连接池,用来管理数据库的连接对象,是springboot默认内部整合的连接池,该连接池号称世界上最快的连接池,底层仍然采用c3p0来管理数据库的连接对象

4.2测试静态资源能否正常加载

将静态资源(SpringBoot电脑商城项目-V1.0\tools\pages_src\pages*)复制到static目录下重启项目并尝试访问localhost:8080/web/login.html(因为static是默认根目录,所以不是localhost:8080/static/web/login.html)

如果这个过程访问失败,原因是idea对于JS代码的兼容性较差,编写了js代码但是有的时候不能正常去加载,解决办法有以下四种

  • clear-install:依次点击MavenProject->store->Lifecycle->clean,等待清哩项目完毕后点击同目录下的install重新部署

  • idea缓存清理:点击File下的Invalidate Caches/Restart…然后在弹出的窗口中选择Invalidate and Restart,此时就会自动清除缓存并重新启动idea

  • rebuild重新构建:点击工具栏的Build下的Rebuild Project

  • 重启电脑

用户注册功能

1.创建数据表

1.选中数据表:

use store

2.创建t_user表

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-已删除',
	created_user VARCHAR(20) COMMENT '日志-创建人',
	created_time DATETIME COMMENT '日志-创建时间',
	modified_user VARCHAR(20) COMMENT '日志-最后修改执行人',
	modified_time DATETIME COMMENT '日志-最后修改时间',
	PRIMARY KEY (uid)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

tips:

  • 注册页面的确认密码功能多数开发中交给前端做,如果两次密码输入不同就不能将数据传给后台

  • 创建t-user表时`password` CHAR(32) NOT NULL COMMENT ‘密码’,因为password是关键字,所以需要用``号(不是单引号,是esc下面的那个键)并且后面用到该字段时(比如往表中插入数据)也需要用``

  • 创建t_user表时salt CHAR(36) COMMENT ‘盐值’,是为了在用户注册时对用户的密码进行加密操作(后续再讲)

  • 数据库中的性别0代表女,1代表男,数据库中用数字而不是文字是因为前端的性别选项是单选框,提交给后台的是数字

  • 创建t_user表时is_delete INT COMMENT ‘是否删除:0-未删除,1-已删除’,的作用:网站中都有注销账号的功能,大部分的网站并不是真的将用户注销了,而是在下次用户登录时进行验证,如果是0就放行,如果是1就禁止登录

  • 创建t-user表时username VARCHAR(20) NOT NULL UNIQUE COMMENT ‘用户名’,的UNIQUE 作为约束条件使用户名唯一

  • 将来任何一张表都有以下四个字段:

    created_user VARCHAR(20) COMMENT ‘创建人’,

    created_time DATETIME COMMENT ‘创建时间’,

    modified_user VARCHAR(20) COMMENT ‘修改人’,

    modified_time DATETIME COMMENT ‘修改时间’,

    所以为了开发方便可以把这四个字段作为整个实体类

2.创建用户的实体类

1.通过表的结构提取出表的公共字段,放在一个实体类的基类中,起名BaseEntity基类中

public class BaseEntity implements Serializable {
    
    
    private String createdUser;
    private Date createdTime;
    private String modifiedUser;
    private Date emodifiedTime;
/**
 * get,set
 * equals和hashCode
 * toString
 */
}

2.创建用户的实体类,并使其继承BaseEntity基类

public class User extends BaseEntity {
    
    
    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
 * equals和hashCode
 * toString
 */
}

tips:

  • 实体类User因为要在网络中以流的形式传输,所以需要serialize序列化(但因为其继承的父类BaseEntity已经实现序列化,所以就不需要再写implements Serializable)

  • 实体类BaseEntity中自动导入Getter and Setter方法,euqals()方法,hashCode()方法,toString方法,其中euqals()方法,hashCode()方法自动导入步骤:

    1. enter+insert
    2. 点击euqals() and hashCode()
    3. 勾选Accept…和Use这两段话,并且选择Template为IntelliJ Default
    4. 一路next到底
  • ssm框架开发项目的时候需要在实体类上面加@Component然后spring才能自动进行对象的创建维护,而springboot不再需要,因为springboot遵循的原则是约定大于配置,如果字段名称相同那就可以自动完成字段的初始化

3.注册-持久层

通过Mybatis来操作数据库,也就是在做mybatis开发的流程

3.1规划需要执行的SQL语句

1.用户的注册功能,从后端持久层来看相当于在做数据的插入操作

inser into t_user (username)

2.在用户的注册时首先要去查询当前的用户名是否存在,如果存在则不能进行注册,相当于是一条查询语句

select * from t_user where username=?

3.2设计接口和抽象方法及实现

1.定义Mapper接口.在项目的目录结构下首先创建一个mapper包,在这个包下再根据不同的功能模块来创建mapper接口.注册功能需要在mapper包下创建UserMapper接口然后定义上述两个SQL语句的抽象方法

public interface UserMapper {
    
    

    /**
     * 插入用户的数据
     * @param user 用户的数据
     * @return 受影响的行数(增删改都将受影响的行数作为返回值,可以根据返回值来判断是否执行成功)
     */
    Integer insert(User user);

    /**
     * 根据用户名来查询用户的数据
     * @param username 用户名
     * @return 如果找到对应的用户则返回这个用户的数据,如果没有找到则返回null
     */
    User findByUsername(String username);
}

2.ssm框架开发项目的时候需要在mapper接口上加@Mapper用于自动生成相应的接口实现类,在springboot也可以这样,但是后续会有很多mapper接口,每个接口分别加@Mapper太麻烦了,所以在启动类类里面指定当前项目的mapper接口在哪,然后项目启动的时候会自动加载所有的接口

@MapperScan("com.cy.mapper")

3.3编写映射

1.定义xml映射文件,与对应的接口进行关联.所有的映射文件都属于资源文件,需要放在resources目录下,为了管理方便我们在resources目录下创建一个mapper文件夹,然后在这个文件夹里存放mapper的映射文件

2.创建接口的映射文件,需要和接口的名称保持一致.如UserMapper.xml

UserMapper.xml的配置在Mybatis官网

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace用于指定当前的映射文件和哪个接口进行映射,需要指定接口的文件路径,路径需要是包的完整路径结构-->
<mapper namespace="com.cy.store.mapper.UserMapper">

</mapper>

3.将配置接口的方法对应到SQL语句上

  • insert into () values (),因为values后面插入的值是动态值,mybatis规定需要用占位符来占位,并给占位符起一个变量的名字,且变量的名字需要在占位符#{}内部
  • 创建t_user表时uid INT AUTO_INCREMENT COMMENT ‘用户id’,中的AUTO_INCREMENT表示主键uid自增,所以需要useGeneratedKeys和keyProperty
<!--在sql语句的最上面借助ResultMap标签来自定义映射规则
    id属性:表示给这个映射规则分配一个唯一的id值,对应的就是resultMap="id属性值"
    type属性:取值是一个类,表示数据库中的查询结果与java中哪个实体类进行结果集的映射
 -->
<resultMap id="UserEntityMap" type="com.cy.store.entity.User">
    <!--将表的字段和类的属性名不一致的进行匹配指定,名称一致的也可以指定,但没必要
        但是,在定义映射规则时无论主键名称是否一致都不能省
        column属性:表示表中的字段名称
        property属性:表示类中的属性名称
        -->
    <id column="uid" property="uid"></id>
    <result column="is_delete" property="isDelete"></result>
    <result column="created_user" property="createdUser"></result>
    <result column="created_time" property="createdTime"></result>
    <result column="modified_user" property="modifiedUser"></result>
    <result column="modified_time" property="modifiedTime"></result>
</resultMap>

<!--id属性:表示映射的接口中方法的名称,直接标签的内容部来编写SQL语句-->
<!--useGeneratedKeys="true"表示开启某个字段的值递增(大部分都是主键递增)
    keyProperty="uid"表示将表中哪个字段进行递增
    -->
<insert id="insert" useGeneratedKeys="true" keyProperty="uid">
    insert into t_user(
        username,`password`,salt,phone,email,gender,avatar,is_delete,
        created_user,created_time,modified_user,modified_time
    ) values (
    #{username},#{password},#{salt},#{phone},#{email},#{gender},#			{avatar},#{isDelete},#{createdUser},#{createdTime},#{modifiedUser},#	{modifiedTime}
    )
</insert>


<!--select语句在执行的时候查询的结果无非两种:一个对象或多个对象
    resultType:表示查询的结果集类型,用来指定对应映射类的类型,且包含完整的包结构,但此处不能是resultType="com.cy.store.entity.User",因为这种写法要求表的字段的名字和类的属性名一模一样
    resultMap:表示当表的字段和类的对象属性名不一致时,来自定义查询结果集的映射规则
-->
<select id="findByUsername" resultMap="UserEntityMap">
    select * from t_user where username=#{username}
</select>

sql语句匹配规则:如果在insert标签里面写了insert语句,首先将insert语句和某一个方法进行绑定,用到了id=“”,但是和哪里的方法进行绑定呢,就要用到namespace=“”,这两步映射就把唯一的SQL语句和唯一的方法进行了关联,实际上就是jdbc里面dao接口的的:

Integer insert(User user) {
    
    
    String SQL = "insert into () values ()";
}

用到映射的好处:使SQL语句和java代码分离,解耦了,方便后期代码的维护

4.将mapper文件的位置注册到properties对应的配置文件中.

在application.properties文件中增添:

mybatis.mapper-locations=classpath:mapper/*.xml

3.4单元测试

1.每个独立的层编写完毕后需要编写单元测试方法来测试当前的功能:在test包结构下创建一个mapper包,在这个包下再创建持久层的功能测试,单元测试方法是独立运行,不用启动整个项目,提高了代码的测试效率

2.因为测试方法要用到mapper层的接口来访问刚刚写的两个方法,所以要在类里面声明UserMapper对象:即private UserMapper userMapper;且需要加上@Autowired完成值的初始化,但此时会发现提示"Could not autowire.No beans of’UserMapper’type found",报错原因是idea有自动检测的功能,在java中接口是不能够直接创建bean的,所以idea认为这个语法不合理,但本质上在项目启动时mybatis自动创建了接口的动态代理实现类,所以从项目的运行角度讲这不能算是错.解决办法:

  • 在Settings里面搜索inspections,依次找到Spring->Spring Core->Code->Autowiring for Bean Class然后将Severity的Error改为Warning
//@SpringBootTest表示当前的类是一个测试类,不会随同项目一块打包
@SpringBootTest

/**
 * 1.@RunWith表示启动这个单元测试类,否则这个单元测试类是不能运行的,需要传递
 * 一个参数,该参数必须是SpringRunner.class即SpringRunner的实例类型
 * 2.敲完@RunWith(SpringRunner.class)后鼠标分别放在SpringRunner和@RunWith上按alt+enter分别导入包
 * 3.单元测试类中出现的方法必须是单元测试方法
 * 4.单元测试方法的特点:必须被@Test注解修饰;返回值类型必须是void;方法的参数列表不指定任何类型;方法的访问修饰符必须是public
 */
@RunWith(SpringRunner.class)
public class UserMapperTests {
    
    

    @Autowired
    private UserMapper userMapper;

    @Test
    public void insert() {
    
    
        User user = new User();
        user.setUsername("张三");
        user.setPassword("123456");
        Integer rows = userMapper.insert(user);
        System.out.println(rows);
    }

    @Test
    public void findByUsername() {
    
    
        User user = userMapper.findByUsername("张三");
        System.out.println(user);
    }
}

4.注册-业务层

业务层的核心功能:

  • 接受前端从控制器流转过来的数据
  • 结合真实的注册业务来完成功能业务逻辑的调转和流程

所以这里要考虑到真实的业务场景,如果只考虑业务场景的话不完整,因为在整个业务执行的过程中会产生很多问题,从java角度来讲这些都是属于异常,所以在业务开发的时候就要规划相关的异常,以便把项目的错误控制在一定范围内

service下的目录结构(建议这样):

  • service包下创建ex包用来写异常类
  • service包下创建impl包用来写接口的实现类
  • 接口直接写在service包下,不再需要接口包

4.1规划异常

1.为什么会有异常:

比如,用户在进行注册时可能会产生用户名被占用的错误,这时需要抛出一个异常

2.怎么处理异常:

  • 异常不能用RuntimeException,太笼统了,开发者没办法第一时间定位到具体的错误类型上,我们可以定义具体的异常类型来继承这个异常.
  • 正常的开发中异常又要分等级,可能是在业务层产生异常,可能是在控制层产生异常,所以可以创建一个业务层异常的基类,起名ServiceException异常,并使其继承RuntimeException异常
  • 后期开发业务层时具体的异常可以再继承业务层的异常ServiceException

3.处理异常的具体步骤:

步骤一:在ex包下创建ServiceException类作为业务层异常的基类:

/**
 * 因为整个业务的异常只有一种情况下才会产生:只有运行时才会产生,不运行不会产生
 * 所以要求业务层的异常都要继承运行时异常RuntimeException并且重写父类的所有构造方法以便后期能抛出自已定义的异常
 */
public class ServiceException extends RuntimeException{
    
    
    //什么也不返回
    public ServiceException() {
    
    
        super();
    }

    //返回异常信息(常用)
    public ServiceException(String message) {
    
    
        super(message);
    }

    //返回异常信息和异常对象(常用)
    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异常基类:

  • 用户在进行注册时可能会产生用户名被占用的错误,这时需要抛出一个UsernameDuplicatedException异常
public class UsernameDuplicatedException extends ServiceException{
    
    
    /**重写ServiceException的所有构造方法*/
}

  • 正在执行数据插入操作的时候,服务器宕机或数据库宕机.这种情况是处于正在执行插入的过程中所产生的异常,起名InsertException异常
//数据插入过程中产生异常
public class InsertException extends ServiceException{
    
    
    /**重写ServiceException的所有构造方法*/
}

4.2设计接口和抽象方法

1.在service包下创建IUserService接口(接口命名的默认规则:I+业务名字+层的名字)

/**用户模块业务层接口*/
public interface IUserService {
    
    
    /**
     * 用户注册方法
     * @param user 用户的数据对象
     */
    void reg(User user);
}

2.创建一个实现UserServiceImpl类,需要实现IUserService接口,并且实现抽象的方法

因为要将这个实现类交给spring管理,所以需要在类上加@Service

@Service
public class UserServiceImpl implements IUserService {
    
    

    //reg方法核心就是调用mapper层的方法,所以要声明UserMapper对象并加@Autowired注解
    @Autowired
    private UserMapper userMapper;

    @Override
    public void reg(User user) {
    
    
        //通过user参数来获取传递过来的username
        String username = user.getUsername();
        //调用mapper的findByUsername(username)判断用户是否被注册过了
        User result = userMapper.findByUsername(username);
        //判断结果集是否为null,不为null的话则需抛出用户名被占用的异常
        if (result != null) {
    
    
            //抛出异常
            throw new UsernameDuplicatedException("用户名被占用");
        }

        /**
         * 密码加密处理作用:
         * 1.后端不再能直接看到用户的密码2.忽略了密码原来的强度,提升了数据安全性
         * 密码加密处理的实现:
         * 串+password+串->交给md5算法连续加密三次
         * 串就是数据库字段中的盐值,是一个随机字符串
         */
        String oldpassword = user.getPassword();
        //1.随机生成一个盐值(大写的随机字符串)
        String salt = UUID.randomUUID().toString().toUpperCase();
        //2.将密码和盐值作为一个整体进行加密处理
        String md5Password = getMD5Password(oldpassword, salt);
        //3.将盐值保存到数据库
        user.setSalt(salt);
        //4.将加密之后的密码重新补全设置到user对象当中
        user.setPassword(md5Password);

        //补全数据:is_delete设置为0
        user.setIsDelete(0);
        //补全数据:四个日志字段信息
        user.setCreatedUser(user.getUsername());
        user.setModifiedUser(user.getUsername());
        Date date = new Date();//java.util.Date
        user.setCreatedTime(date);
        user.setModifiedTime(date);


        //执行注册业务功能的实现
        Integer rows = userMapper.insert(user);
        if (rows != 1) {
    
    
            throw new InsertException("在用户注册过程中产生了未知的异常");
        }
    }
}

md5加密算法以后可能还要多次用到,为了方便在UserServiceImpl类里面单独写一个getMD5Password方法

private String getMD5Password(String password,String salt) {
    
    
    for (int i = 0; i < 3; i++) {
    
    
        password = DigestUtils.md5DigestAsHex((salt + password + salt).getBytes()).toUpperCase();
    }
    return password;
}

4.3单元测试

在单元测试包下创建一个UserServiceTests类,在这个类中添加单元测试的功能(技巧:可以先在test.com.cy.store下创建service包,然后点击UserMapperTests并ctrl+c,然后点击service包ctrl+v会弹出修改类名,将UserMapperTests改为UserServiceTests即可,然后修改部分代码至如下这样)

@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTests {
    
    

    @Autowired
    private IUserService userService;

    @Test
    public void reg() {
    
    
        /**
         * 进行插入时可能会出错抛出异常,这时需要捕获异常:
         * 1.选中    User user = new User();
         *           user.setUsername("张7");
         *           user.setPassword("123456");
         *           userService.reg(user);
         *           System.out.println("OK");
         * 2.点击导航栏的Code,然后依次点击SurroundWith->try/catch就可以捕获异常了
         * 3.Exception e没有问题,但这里我们知道是Service层的异常,所以可以改为ServiceException e
         * 4.System.out.println(e.getClass().getSimpleName());获取异常对象再获取类的名称然后输出
         * 5.System.out.println(e.getMessage());输出异常信息(是自己在ServiceException的子类类具体设置的信息)
         */
        try {
    
    
            User user = new User();
            user.setUsername("张7");
            user.setPassword("123456");
            userService.reg(user);
            System.out.println("OK");
        } catch (ServiceException e) {
    
    
            System.out.println(e.getClass().getSimpleName());
            System.out.println(e.getMessage());
        }
    }
}

5.注册-控制层

5.1创建响应

状态码,状态描述信息,数据是所有控制层对应的方法都涉及到的操作,所以把这部分功能封装到一个类JsonResult中,将这个类作为方法的返回值返回给前端浏览器:

//因为所有的响应的结果都采用Json格式的数据进行响应,所以需要实现Serializable接口
public class JsonResult<E> implements Serializable {
    
    
    //状态码
    private Integer state;
    //描述信息
    private String message;
    //数据类型不确定,用E表示任何的数据类型,一个类里如果声明的有泛型的数据类型,类也要声明为泛型
    private E data;
	
    //无参构造
    public JsonResult() {
    
    
    }

    //将状态码传给构造方法初始化对象
    public JsonResult(Integer state) {
    
    
        this.state = state;
    }


    //将状态码和数据传给构造方法初始化对象
    public JsonResult(Integer state, E data) {
    
    
        this.state = state;
        this.data = data;
    }

    //如果有异常,直接将异常传递给构造方法初始化对象
    public JsonResult(Throwable e) {
    
    
        this.message=e.getMessage();
    }
    /**以及属性的get和set方法*/
}

5.2设计请求

接下来该向后端服务器发送请求以把用户数据插入到数据库,设计发送请求模块的第一步就是设计相关的请求

依据当前的业务功能模块进行请求的设计:

  • 请求路径:/users/reg
  • 请求参数:User user
  • 请求类型:POST
  • 响应结果:JsonResult<void>

5.3处理请求

创建一个控制层对应的UserController类,依赖于业务层的接口.编写完成后启动主服务验证一下

@RestController //其作用等同于@Controller+@ResponseBody
//@Controller
@RequestMapping("users")
public class UserController {
    
    

    @Autowired
    private IUserService userService;

    @RequestMapping("reg")
    //@ResponseBody //表示此方法的响应结果以json格式进行数据的响应给到前端
    public JsonResult<Void> reg(User user) {
    
    
        //创建响应结果对象即JsonResult对象
        JsonResult<Void> result = new JsonResult<>();
        try {
    
    
            //调用userService的reg方法时可能出现异常,所以需要捕获异常
            userService.reg(user);
            result.setState(200);
            result.setMessage("用户注册成功");
        } catch (UsernameDuplicatedException e) {
    
    
            result.setState(4000);
            result.setMessage("用户名被占用");
        } catch (InsertException e) {
    
    
            result.setState(5000);
            result.setMessage("注册时产生未知的异常");
        }
        return result;
    }
}

5.4控制层优化设计

凡是业务层抛出的异常我们都在控制层进行了捕获,如果其他的业务模块也抛用户名被占用或者插入时异常,那么抛出异常的代码就要重复编写

优化方法:在控制层抽离出一个BaseController父类,在这个父类中统一处理关于异常的相关操作,优化如下:

1.在controller包下创建UserController类作为控制层下类的基类,用来做统一的异常捕获:

public class BaseController {
    
    

    //操作成功的状态码
    public static final int OK = 200;

    /**
     * 1.@ExceptionHandler表示该方法用于处理捕获抛出的异常
     * 2.什么样的异常才会被这个方法处理呢?所以需要ServiceException.class,这样的话只要是抛出ServiceException异常就会被拦截到handleException方法,此时handleException方法就是请求处理方法,返回值就是需要传递给前端的数据
     * 3.被ExceptionHandler修饰后如果项目发生异常,那么异常对象就会被自动传递给此方法的参数列表上,所以形参就需要写Throwable e用来接收异常对象
     */
    @ExceptionHandler(ServiceException.class)
    public JsonResult<Void> handleException(Throwable e) {
    
    
        JsonResult<Void> result = new JsonResult<>(e);
        if (e instanceof UsernameDuplicatedException) {
    
    
            result.setState(4000);
            result.setMessage("用户名已经被占用");
        } else if (e instanceof InsertException) {
    
    
            result.setState(5000);
            result.setMessage("插入数据时产生未知的异常");
        }
        return result;
    }
}

2.让UserController继承BaseController并重构UserController下的reg方法使该方法只需要关注请求处理而不再需要关注异常捕获:

public JsonResult<Void> reg(User user) {
    
    
    userService.reg(user);
    return new JsonResult<>(OK);
}

6.注册-前端页面

6.1熟悉ajax

1.什么是ajax函数?

这是jQuery封装的一个函数,称为$.ajax()函数,通过对象调用ajax()函数用来异步加载相关的请求.依靠的是JavaScript提供的一个对象:XHR(全称XmlHttpResponse)

2.ajax()函数的语法结构:

  • 使用ajax()时需要传递一个方法体作为方法的参数来使用(一对大括号就是一个方法体)
  • ajax接受多个参数时,参数与参数之间使用","分割
  • 每一组参数之间使用":"进行分割
  • 参数的组成部分一个是参数的名称(不能随便定义),另一个是参数的值(必须用字符串来表示)
  • 参数的声明顺序没有要求

演示一下语法结构:

$.ajax({
    
    
    url: "",
    type: "",
    data: "",
    dataType: "",
    success: function() {
    
    
        
    },
    error: function() {
    
    
        
    }
});

3.ajax()函数参数的含义:

参数 功能描述
url 表示请求的地址(url地址),例如:url:“localhost:8080/users/reg”(1.不能包含参数列表部分的内容2.如果提交的请求是项目内部的一个url,那么端口号前面的都可以省略掉,即url:“/users/reg”)
type 请求类型(GET和POST请求的类型).例如:type:“POST”(get和post不区分大小写)
data 向指定的请求url地址提交的数据.例如:data:“username=tom&pwd=123”
dataType 提交的数据的类型.数据的类型一般指定为json类型.例如:dataType:“json”(json不区分大小写)
success 当服务器正常响应客户端时,会自动调用success参数的方法,并且将服务器返回的数据以参数的形式传递给这个方法的参数上
error 当服务器未正常响应客户端时,会自动调用error参数的方法,并且将服务器返回的数据以参数的形式传递给这个方法的参数上

6.2前端js编写

js代码可以独立声明在一个js文件里或者声明在一个script标签中.现在我们在register.html中编写js代码,js代码可以放在head标签中,也可以放在body标签中,可以放在任意一个位置,只要被script标签包裹就行了,这里我们放在整个body结束之前:

        <script>
            //1.监听注册按钮是否被点击,如果被点击可以执行一个方法(这里不能像ajax函数那样删去function()只留下{},这是官方规定的!)
            $("#btn-reg").click(function () {
    
    

                //let username = $("#username").val();
                //let pwd = $("#password").val();
                //上面这两行是动态获取表单中控件的数据,但是如果这样获取的话ajax函数中
                //就是data: "username="+username + "&password="+pwd,但太麻烦了,如
                // 果这个表单提交的是用户的兴趣爱好,那数据就很多了,一个表单20个数据都很正
                // 常,如果此时还用这种方式就太麻烦了,所以不建议用这种方式

                //2.发送ajax()的异步请求来完成用户的注册功能
                $.ajax({
    
    
                    url: "/users/reg",
                    type: "POST",

                    //serialize这个API会自动检测该表单有什么控件,每个控件检测后还会获取每个控
                    // 件的值,拿到这个值后并自动拼接成形如username=Tom&password=123的结构
                    data: $("#form-reg").serialize(),

                    dataType: "JSON",
                    success: function (json) {
    
     //1.js是弱数据类型,这个地方不用声明json的数据类型
                        //2.如果服务器成功响应就会将返回的数据传给形参,比如{state: 4000,message: "用户名
                        // 已经被占用",data: null}
                        if (json.state == 200) {
    
    
                            alert("注册成功")
                        } else {
    
    
                            alert("注册失败")
                        }
                    },
                    error: function (xhr) {
    
     //如果问题不在可控范围内,服务器就不会返回自己定
                        //义的json字符串:{state: 4000,message: "用户名已经被占用",data: null}
                        //而是返回一个XHR类型的对象,该对象也有一个状态码名字是status
                        alert("注册时产生未知的错误!"+xhr.status);
                    }
                });
            });
        </script>

此时可能会出现点击注册提交表单时没有任何响应,原因是idea对于JS代码的兼容性较差,编写了js代码但是有的时候不能正常去加载,解决办法有四种,同前面的:项目环境搭建->项目测试->测试静态资源能否正常加载

包括以后如果修改了前端页面,测试时没有报错也没有按照预想的响应,就考虑是编写的js代码还没有被加载,尝试用这四种方法解决

猜你喜欢

转载自blog.csdn.net/maxiangyu_/article/details/124070503