电子商城记录-用户注册

目录

1 用户-创建数据表

2 创建用户实体类

3 持久层

1 准备

2 规划需要执行的SQL语句

3 接口与抽象方法

4 配置SQL映射

4 业务层

1 规划异常

2 设计接口和抽象方法

3 实现接口方法

4 设置MD5加密密码

5 测试检查

5 控制层

1 创建响应

2 设计请求

3 处理请求

4 控制层优化设计

6 前端页面


1 用户-创建数据表

创建数据库并创建数据表,在库中创建数据库

2 创建用户实体类

在创建用户实体类前,观察是否有通用基类分离,项目中许多实体类都会有日志相关的四个属性,所以在创建实体类之前,应先创建这些实体类的基类,将4个日志属性声明在基类中。

1.entity包下创建BaseEntity类,作为实体类的基类。因为这个基类的作用就是用于被其它实体类继承的,所以应声明为抽象类。

2.创建用户数据的实体类User,继承自BaseEntity类,并在类中声明与数据库对应的属性,创建

  • Getter and Setter、

  • Generate hashCode() and equals():比较值是否相等定义比较规则,hashcode地址输出、

  • toString():方便测试使用

    等方法,

熟练可用lombok通过注解的形式自动生成构造器,提高开发效率,不过新手可以手动创建,提高源代码的可读性和完整性。

3.注:用户的实体类 :SpringBoot 约定大于配置,在spring boot中提供了一套默认配置,不需要手动去写xml配置文件,只有默认配置不能满足我们的需求时,才会去修改配置。如果没有配置,会取默认值,而所取的默认值就是约定,具体可以了解“约定大于配置“这个概念。

3 持久层

1 准备

1.测试类中编写并执行“获取数据库连接”的单元测试,以检查数据库连接的配置是否正确。

注意application文件的配置。

2.执行测试类中的contextLoads()测试方法,以检查测试环境是否正常。

2 规划需要执行的SQL语句

1.用户注册的本质是向用户表中插入数据。需要执行insert语句。

2.观察表中属性设计,是否有UNIQUE属性,在执行插入数据之前,还应该检查该用户名是否已经被注册,因此需要有“根据用户名查询用户数据”的功能。

3 接口与抽象方法

1.创建UserMapper接口,添加抽象方法。

  • UserMapper接口是处理用户数据操作的持久层接口

    • insert 抽象方法(插入用户数据):接收用户数据,返回受影响的行数(增,删,改都有受影响,将行数作为返回值,根据返回值判断是否执行成功)

      插入用户数据字段多,声明麻烦,高耦合,所以设计为User对象

    • findByUsername 抽象方法(根据用户名来查询用户数据):传入用户名,如果找到对应用户则返回这个用户的数据,如果没有找到则返回null值

2.第一次创建持久层接口,应在StoreApplication启动类之前添加@MapperScan("mapper包位置")注解,以配置接口文件的位置。

  • @MapperScan注解:指定当前项目中的Mapper接口路径的位置,在项目启动时候会自动加载所有接口文件

注:MyBatis与Spring整合后需要实现实体和数据表的映射关系。实现实体和数据表的映射关系可以在Mapper接口上添加@Mapper注解。但建议以后直接在SpringBoot启动类中加@MapperScan("mapper包") 注解,这样会比较方便,不需要对每个Mapper都添加@Mapper注解。

4 配置SQL映射

1.在resources下创建mapper文件夹,并在该文件夹下创建UserMapper.xml映射文件。对UserMapper接口下的两个抽象方法进行映射配置。

  • namespace属性:用于指定当前映射文件和哪些接口进行映射,需要指定接口的文件路径,需要标注包的完整路径接口

<mapper namespace="UserMapper路径">

由于数据库与Java命名规则不同,驼峰命名法,需要将表的字段和和类的属性不一致的字段进行匹配指定,名称一致的字段可以省略不 写

  • 自定义映射规则:resultMap标签来完成映射规则的定义

    • 核心属性1:id属性:表示个i这个映射规则分配一个唯一的id值,对应的就是resultMap="id属性的值"属性的取值

    • 核心属性2: type属性:一个类,表示的是数据库中的查询结果与java中哪个实体类进行结果集的映射

  • 配合完成名称不一致的映射: column属性:表中的字段名称 property属性:表示类中的属性名称(User实体类属性名称)

    注:表示映射的接口中方法的名称,直接在标签的内部来编写SQL语句.属性建议直接复制

2.项目中第一次使用SQL映射,需要在application.properties中添加mybatis.mapper-locations属性的配置,以指定XML文件的位置。不然会出现“ Invalid bound statement (not found):mapper路径”错误提示。

3.完成抽象方法映射后及时执行单元测试,检查所开发的功能是否可正确运行。创建UserMapperTests单元测试类,在测试类的声明之前添加@RunWith(SpringRunner.class)和@SpringBootTest注解,并在测试类中声明持久层对象,通过自动装配来注入值。

  • RunWith(): 表示启动这个单元测试类(单元注解声明)是一个测试启动器,可以加载SpringBoot测试注解。如果不写,单元测试类可能无法运行,需要传递一个参数,必须是SpringRunner的实例类型(.class)

注:在idea中@RunWith(SpringRunner.class)可以不加,在正常情况下测试类是需要@RunWith的,作用是告诉java你这个类通过用什么运行环境运行,在正常情况下测试类是需要@RunWith的,作用是告诉java你这个类通过用什么运行环境运行,在IDEA里去掉@RunWith仍然能跑是因为在IDEA里识别为一个JUNIT的运行环境,相当于就是一个自识别的RUNWITH环境配置。但在其他IDE里并没有。

来自:关于@RunWith(SpringRunner.class)的作用_赢啦啦的博客-CSDN博客_springrunner作用

4.自动装配userMapper时,报“Could not autowire. No beans of 'UserMapper' type found”错,无法进行自动装配。

解决方案:将setting下搜索inspections,在spring core下的code中找到Autowiring for bean class选项,将Severity设置为Warning即可。如果没有Autowiring for bean class选项,将incorrect injection point autowiring in spring bean components下的Severity设置为Warning即可,加注解可以解决该问题,但加注解相对麻烦 更改等级权限是全局写法。

5.编写两个测试方法,对完成的两个功能进行单元测试。

注:单元测试方法必须为public修饰,方法的返回值类型必须为void,方法不能有参数列表,并且方法被@Test注解修饰。

tips:设置单元测试方法优点独立运行,不用启动整个项目,可以做单元测试,提升了代码的测试效率

特点: 1.必须被Test注解修饰 2.返回值类型必须是void 3.方法的参数列表不能指定任何类型 4.方法的访问修饰符必须是public

满足以上4个特点就可独立运行。在单元测试类中出现方法必须是单元测试方法。

4 业务层

1 规划异常

业务执行产生问题,java角度属于异常

规划异常目的:将错误控制在范围内

如何规划:

1.RuntimeException异常,作为者异常的子类,然后再去定义具体的异常类型来继承这个异常。异常也有分等级,在业务层异常的基 类,ServiceException异常。这个异常继承RuntimeException异常。在有具体异常,继承ServiceException异常。异常机制建立

tips:直接抛java所提供的异常可以吗?结构上没问题,但这种异常过于笼统,不明确,插入过程中用户名占用,密码错误,宕机都属 于异常,RuntimeException异常,没有办法第一时间由开发者快速定位错误类型,效率降低。

public class ServiceException extends RuntimeException{
    //定义子类中的构造方法,重写父接口5个构造方法
​
    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

2.用户在注册的时候可能会产生用户名被占用的错误,抛出一个异常:UsernameDuplicatedException 异常

package com.cy.store.service;
​
public class UsernameDuplicatedException extends ServiceException{
    public UsernameDuplicatedException() {
        super();
    }
​
    public UsernameDuplicatedException(String message) {
        super(message);
    }
​
    public UsernameDuplicatedException(String message, Throwable cause) {
        super(message, cause);
    }
​
    public UsernameDuplicatedException(Throwable cause) {
        super(cause);
    }
​
    protected UsernameDuplicatedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}
​

3.正在执行数据插入操作,服务器,数据库宕机。处于正在插入的过程中所产生的异常。

InsertException异常

package com.cy.store.service;
​
public class InsertException extends ServiceException{
    public InsertException() {
        super();
    }
​
    public InsertException(String message) {
        super(message);
    }
​
    public InsertException(String message, Throwable cause) {
        super(message, cause);
    }
​
    public InsertException(Throwable cause) {
        super(cause);
    }
​
    protected InsertException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

2 设计接口和抽象方法

命名:以大写i “I”开头,跟上业务名称,业务层,在跟上service

1.在service包下创建一个IUserService接口,在接口中添加抽象方法

package com.cy.store.service.impl;
​
import com.cy.store.entity.User;
​
/**用户模块业务层接口*/
public interface IUserService {
    //定义方法,完成注册,注册时业务层不需要返回值
​
    /**
     * 用户注册方法
     * @param user 用户的数据对象
     */
    void reg(User user);
}

2.创建一个实现类UserServiceImpl类,实现这个接口,并且实现抽象方法。创建业务层接口目的是为了解耦。

  • 1.仅以操作成功为前提来设计返回值类型,不考虑操作失败的情况;

  • 2.方法名称可以自定义,通常与用户操作的功能相关;

  • 3.方法的参数列表根据执行的具体业务功能来确定,需要哪些数据就设计哪些数据。通常情况下,参数需要足以调用持久层对应的相关功能;同时还要满足参数是客户端可以传递给控制器的;

  • 4.方法中使用抛出异常的方式来表示操作失败。

业务逻辑:异常的处理和捕获

3 实现接口方法

1.在service.impl.UserServiceImpl业务层下创建实现类,并实现IUserService接口。在类之前添加@Service注解,并在类中添加持久层UserMapper对象。

package com.cy.store.service.impl;
​
import com.cy.store.entity.User;
import com.cy.store.mapper.UserMapper;
import com.cy.store.service.IUserService;
import com.cy.store.service.ex.InsertException;
import com.cy.store.service.ex.UsernameDuplicatedException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
​
import java.util.Date;
import java.util.UUID;
​
/**
 * 用户模块业务层的实现类
 */
//@Service 注解:将当前类的对象交给spring管理,自动创建对象以及对象的维护
// 不加,报错 UnsatisfiedDependencyException 依赖异常 NoSuchBeanDefinitionException 没有找到接口的实现类 并没有交给spring管理
@Service
public class UserServiceImpl implements IUserService {
    public void reg(User user) {
    
    }
}

2.reg()具体实现

package com.cy.store.service.impl;
​
import com.cy.store.entity.User;
import com.cy.store.mapper.UserMapper;
import com.cy.store.service.IUserService;
import com.cy.store.service.ex.InsertException;
import com.cy.store.service.ex.UsernameDuplicatedException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
​
import java.util.Date;
import java.util.UUID;
​
/**
 * 用户模块业务层的实现类
 */
//@Service 注解:将当前类的对象交给spring管理,自动创建对象以及对象的维护
// 不加,报错 UnsatisfiedDependencyException 依赖异常 NoSuchBeanDefinitionException 没有找到接口的实现类 并没有交给spring管理
@Service
public class UserServiceImpl implements IUserService {
    @Autowired
    private UserMapper userMapper;
​
    /***
     * 调用mapper层方法,把user对象传递下去
     * 业务逻辑:异常的处理和捕获
     * @param user 用户的数据对象
     */
    @Override
    public void reg(User user) {
        //通过user参数来获取传递过来的username获取注册的用户名
        String username = user.getUsername();
        //调用持久层的User findByUsername(username)判断用户是否被注册过
        //判断有无拿到,使用结构集接收
        User result = userMapper.findByUsername(username);
        //判断结果集是否不为null则抛出用户名被占用的异常,为null执行注册逻辑
        if(result != null){
            //抛出异常
            throw new UsernameDuplicatedException("用户名被占用");
        }
​
        // 密码加密处理实现(user交给insert之前):MD5算法的形式
        // (串 + password + 串) ------ 再将整体由MD5加密,连续加载3次
        //盐值 + password + 盐值 ----盐值就是一个随机的字符串
        String oldPassword = user.getPassword();
        //获取盐值(随机生成一个盐值)
        String salt = UUID.randomUUID().toString().toUpperCase();
        // 补全数据:盐值的记录
        user.setSalt(salt);
        //将密码和盐值作为一个整体进行加密处理,忽略原有密码的强度,提升数据的安全性
        String md5Password = getMD5Password(oldPassword, salt);
        //将加密密码重新补全设置到user对象中
        user.setPassword(md5Password);
​
​
        // user数据补全操作:is_delete设置为0
        user.setIsDelete(0);
        // user数据补全操作:4个日志字段信息
        user.setCreatedUser(user.getUsername());
        user.setModifiedUser(user.getUsername());
        Date date = new Date();
        user.setCreatedTime(date);
        user.setModifiedTime(date);
​
        // 执行注册业务逻辑功能的实现(rows == 1)插入成功
        Integer rows = userMapper.insert(user);
        if(rows != 1){
            throw new InsertException("在用户注册过程中产生了未知异常");
        }
    }
}

4 设置MD5加密密码

getMD5Password()方法具体实现

 /** 定义一个MD5算法的加密处理*/
    private String getMD5Password(String password,String salt){
        // MD5加密算法方法的调用(进行3次加密)
        for (int i = 0; i <3 ; i++) {
            password = DigestUtils.md5DigestAsHex((salt+password+salt).getBytes()).toUpperCase();
        }
        // 返回加密之后的密码
        return password;
    }
 //密码加密处理实现(user交给insert之前):MD5算法的形式
        // (串 + password + 串) ------ 再将整体由MD5加密,连续加载3次
        //盐值 + password + 盐值 ----盐值就是一个随机的字符串
        String oldPassword = user.getPassword();
        //获取盐值(随机生成一个盐值)
        String salt = UUID.randomUUID().toString().toUpperCase();
        // 补全数据:盐值的记录
        user.setSalt(salt);
        //将密码和盐值作为一个整体进行加密处理,忽略原有密码的强度,提升数据的安全性
        String md5Password = getMD5Password(oldPassword, salt);
        //将加密密码重新补全设置到user对象中
        user.setPassword(md5Password);

5 测试检查

在单元测试包下创建一个UserServiceTest类,在这个类中添加单元测试功能,执行操作,检测数据库。

package com.cy.store.service;
​
​
import com.cy.store.entity.User;
import com.cy.store.mapper.UserMapper;
import com.cy.store.service.ex.ServiceException;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
​
//@SpringBootTest:表示标注当前的类是一个测试类,不会随同项目一块打包发送
@SpringBootTest
//RunWith():表示启动这个单元测试类(单元注解声明)(如果不写,单元测试类无法运行),需要传递一个参数,必须是SpringRunner的实例类型(.class)
//@RunWith(SpringRunner.class)
public class UserServiceTests {
    //报错“Could not autowire. No beans of 'UserMapper' type found”
    /**
     * 原因:idea有检测功能,接口是不能够直接创建Bean的(动态代理技术解决),认为语法不合理
     * 本质项目启动,接口能运行的原因:mybatis创建接口动态代理实现类,完成对象的创建
     */
    @Autowired
    private IUserService userService;
    /**
     *  单元测试方法特点:满足以下4个特点就可以单独独立运行,不用启动整个项目,可以做单元测试,提升了代码的测试效率
     *  1.必须被Test注解修饰
     *  2.返回值类型必须是void
     *  3.方法的参数列表不能指定任何类型
     *  4.方法的访问修饰符必须是public
     *
     *  在单元测试类中出现方法必须是单元测试方法
     */
    @Test
    public void reg(){
        try {
            User user = new User();
            user.setUsername("john02");
            user.setPassword("123");
            userService.reg(user);// Integer rows观察影响的行数
            System.out.println("ok");
        } catch (ServiceException e) {
            // 获取类的对象,在获取类的名称
            System.out.println(e.getClass().getSimpleName());
            // 获取异常的具体描述信息
            System.out.println(e.getMessage());
        }
    }
}

5 控制层

1 创建响应

  • 状态码

  • 状态描述信息

  • 响应数据

所有控制层对应这三方面操作,减少开发,将这部分功能封装再一个类中,将这个类作为方法返回值,返回给前端浏览器。

public class JsonResult<E> implements Serializable {
    /** 状态码 */
    private Integer state;
    /** 描述信息 */
    private String message;
    /** 数据,对应数据,数据类型不确定,用泛型表示  */
    private E data;
    }

向后端服务器发送请求,针对请求,将用户数据插入后端,首先是设计请求

2 设计请求

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

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

3 处理请求

1.创建一个控制层对应的类UserController类。依赖于业务层的接口,添加上相应注解,接收相关请求。

package com.cy.store.controller;
​
import com.cy.store.entity.User;
import com.cy.store.service.IUserService;
import com.cy.store.service.ex.InsertException;
import com.cy.store.service.ex.UsernameDuplicatedException;
import com.cy.store.util.JsonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
​
//@Controller
@RestController // 等效 @Controller + @ResponseBody
@RequestMapping("user")// 什么样的请求需要被拦截到子类当中
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(user);
            result.setState(200);
            result.setMessage("用户注册成功");
        } catch (UsernameDuplicatedException e) {
            result.setState(4000);
            result.setMessage("用户名被占用");
        }catch (InsertException e) {
            result.setState(5000);
            result.setMessage("注册时产生未知的异常");
        }
        return result;
    }
}
​

业务逻辑相对麻烦,凡是业务层抛出异常都在控制层捕获,如果其他业务模块也抛用户名占用或插入异常,就会重复编写下面代码

catch (UsernameDuplicatedException e) {
            result.setState(4000);
            result.setMessage("用户名被占用");
        }catch (InsertException e) {
            result.setState(5000);
            result.setMessage("注册时产生未知的异常");
        }

4 控制层优化设计

1.在控制层抽离一个父类,再这个父类中统一的处理关于异常的相关操作。编写一个BaseController,在其中定义表示响应成功的状态码及统一处理异常的方法。

@ExceptionHandler注解用于统一处理方法抛出的异常。当我们使用这个注解时,需要定义一个异常的处理方法,再给这个方法加上@ExceptionHandler注解,这个方法就会处理类中其他方法(被@RequestMapping注解)抛出的异常。@ExceptionHandler注解中可以添加参数,参数是某个异常类的class,代表这个方法专门处理该类异常。

package com.cy.store.controller;
import com.cy.store.service.ex.InsertException;
import com.cy.store.service.ex.ServiceException;
import com.cy.store.service.ex.UsernameDuplicateException;
import com.cy.store.util.JsonResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
​
/** 控制器类的基类 */
public class BaseController {
    /** 操作成功的状态码 */
    public static final int OK = 200;
​
    /** @ExceptionHandler用于统一处理方法抛出的异常 */
    @ExceptionHandler(ServiceException.class)
    public JsonResult<Void> handleException(Throwable e) {
        JsonResult<Void> result = new JsonResult<Void>(e);
        if (e instanceof UsernameDuplicateException) {
            result.setState(4000);
        } else if (e instanceof InsertException) {
            result.setState(5000);
        }
        return result;
    }
}

2.最后简化UserController控制器类中的用户注册reg()方法的代码。

/** 处理用户相关请求的控制器类 */
@RestController
@RequestMapping("users")
public class UserController extends BaseController {
    @Autowired
    private IUserService userService;
​
    @RequestMapping("reg")
    public JsonResult<Void> reg(User user) {
        // 调用业务对象执行注册
        userService.reg(user);
        // 返回
        return new JsonResult<Void>(OK);
    }
}

3.完成后启动项目,打开浏览器访问http://localhost:8080/users/reg?username=controller&password=123456请求进行测试。

6 前端页面

1.编写发送请求

在register页面中编写发送请求的方法 点击事件完成(/users/reg),选中对应的按钮($(选择器)),再去添加点击的事件,$.ajax()函数发送异步请求。

2.JQUery 封装了一个函数 ajax

JQUery 封装了一个函数,称之为 $.ajax()函数,通过对象调用ajax()函数,可以异步加载相关的请求(异步:就是页面不会改变,但是数据会不断加载),依靠的是Javscript提供的一个对象XHR (XmlHttpResponse),封装了这个对象。

3.ajax()使用方式,语法结构

  • ajax()使用方式,需要传递一个方法题作为方法的参数来使用,一对大括号称之为方法体。要求传递一系列参数。

  • ajax接收多个参数,参数与参数之间要求使用英文下的逗号","进行分割;

  • 每一组参数之间使用":"进行分割;

  • 参数的组成部分一个是参数的名称(不能随意的定义),另一个是参数的值,要求使用字符串来标识 “ ”。

  • 参数的声明顺序没有要求。

语法结构:

/**
$.ajax(fun());//不能达到复用
​
function fun(){
    //TODO
}
*/
​
正确:
$.ajax({
    url: "",
    type: "",
    data: "",
    dataType: "",
    success:function(){
        
    },
    error:function(){
        
    }
})

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

参数功能描述
url表示请求的地址(url地址),不能博阿寒参数列表部分的内容。例如:“url:”localhost/users/reg“
type请求的类型(GET和POST)。例如:type:”POST“
data向指定的请求url地址提交的数据。例如:data:”username=tom&pwd=123“
dataType提交的数据的类型。数据的类型一般指定为json类型。例如:dataType:”json“
success当服务器正常响应客户端时,会自动调用success参数的方法,并且将服务器返回的数据以参数的形式传递给这个方法的参数上
error当服务器未正常响应客户端时,会自动调用error参数的方法,并且将服务器返回的数据以参数的形式传递给这个方法的参数上

5.js代码可以独立存放在一个后缀为js的文件里或者声明在一个script标签中。

<script type="text/javascript">
            //1.监听注册按钮是否被点击,借助选择器选中按钮,id选择器以#开头
            //click()监听是否被点击,如果被点击,可以执行一个方法,
            $("#btn-reg").click(function () {
                //动态获取表单中控制的数据
                //let username = $("#username").val();
                //let pwd = $("#password").val();
                console.log($("#form-reg").serialize());//把表单所有的控件输出
​
                //2.发送ajax()的异步请求来完成用户的注册功能,注意记得加逗号分割
                $.ajax({
                    url: "/users/reg",
                    type: "POST",
                    // username=Tom&password=123
                    data: $("#form-reg").serialize(),//先拿到表单,serialize()检测表单有哪些控件
                    //复杂不推荐:data: "username="+username + "&password="+pwd,
                    dataType: "JSON",
                    success:function(json){//success中的state与error中的status不同,error是官网提供
                        if (json.state == 200){
                            alert("注册成功");
                        }else{
                            alert("注册失败");
                        }
                    },
                    error:function(){
                        alert("注册时产生未知的错误!" + xhr.status);
​
                    }
                })
            });
​
        </script>

6.js代码无法正常被服务器解析执行,体现在点击页面中的按钮没有任何响应。

解决方案:

  • 在项目的maven下clear清理项目-install重新部署

  • 在项目的file选项下-cash清理缓存

  • 重新构建项目:build选项下-rebuild选项

  • 重启idea

  • 重启电脑

注意:检查id选择器以#开头