SpringBoot-项目1-用户(注册,登录,修改密码,修改个人资料,上传头像)

1. 项目分析

在设计一款软件时,在编写代码之前,应该先分析这个项目中需要处理哪些类型的数据!例如,本项目中需要处理的数据种类有:收藏,购物车,用户,收货地址,订单,商品,商品类别。

当确定了需要处理的数据的种类之后,就应该确定这些数据的处理先后顺序:用户 > 收货地址 > 商品类别 > 商品 > 收藏 > 购物车 > 订单。

在具体开发某个数据的管理功能之前,还应该分析该数据需要开发哪些管理功能,以用户数据为例,需要开发的有:修改密码,上传头像,修改资料,登录,注册。

分析出功能之后,也需要确定这些功能的开发顺序,一般先开发简单的,也依据增、查、删、改的顺序,则以上功能的开发顺序应该是:注册 > 登录 > 修改密码 > 修改资料 > 上传头像。

在开发某个数据的任何功能之前,还应该先创建这种数据对应的数据表,然后,创建对应的实体类,再开发某个功能!

在开发某个功能时,还应该遵循顺序:持久层(数据库编程) > 业务层 > 控制器层 > 前端页面。

2. 用户-创建数据表

先创建数据库:

CREATE DATABASE db_store;
USE db_store;

然后,在数据库中创建数据表:

CREATE TABLE t_user (
  uid INT AUTO_INCREMENT COMMENT '用户id',
  username VARCHAR(20) UNIQUE NOT NULL COMMENT '用户名',
  password CHAR(32) NOT NULL COMMENT '密码',
  salt CHAR(36) COMMENT '盐值',
  gender INT(1) COMMENT '性别:0-女,1-男',
  phone VARCHAR(20) COMMENT '手机号码',
  email VARCHAR(50) COMMENT '电子邮箱',
  avatar VARCHAR(100) COMMENT '头像',
  is_delete INT(1) COMMENT '是否删除:0-否,1-是',
  created_user VARCHAR(20) COMMENT '创建人',
  created_time DATETIME COMMENT '创建时间',
  modified_user VARCHAR(20) COMMENT '最后修改人',
  modified_time DATETIME COMMENT '最后修改时间',
  PRIMARY KEY (uid)
) DEFAULT CHARSET=utf8mb4;

完成后,可以通过desc t_user;show create table t_user;进行查看。

3. 用户-创建实体类

创建SpringBoot项目,所以,先打开https://start.spring.io创建项目,创建时,使用的版本选择2.1.12,Group为cn.demo,Artifact为store,Packaging为war,添加Mybatis FrameworkMySQL Driver` 依赖,在网站生成项目后,将解压得到的项目文件夹剪切到Workspace中,并在Eclipse中导入该项目。

src/main/java下,在现有的cn.demo.store包中,创建子级entity包,用于存放实体类,先在entity包中创建所有实体类的基类:

/**
 * 实体类的基类
 */
abstract class BaseEntity implements Serializable {

  private static final long serialVersionUID = -3122958702938259476L;
  
  private String createdUser;
  private Date createdTime;
  private String modifiedUser;
  private Date modifiedTime;
  
  // 自行添加SET/GET方法,toString()
  
}

并在entity包中创建User类,继承自以上基类:

/**
 * 用户数据的实体类
 */
public class User extends BaseEntity {

  private static final long serialVersionUID = -3302907460554699349L;
  
  private Integer uid;
  private String username;
  private String password;
  private String salt;
  private Integer gender;
  private String phone;
  private String email;
  private String avatar;
  private Integer isDelete;
 
  // 自行添加SET/GET方法,基于uid的equals()和hashCode()方法,toString()方法
  
}

4. 用户-注册-持久层

持久层:持久化保存数据的层。

刚创建好的SpringBoot项目,由于添加了数据库相关的依赖,在没有配置数据库连接信息之前,将无法启动!所以,应该先在application.properties中添加配置:

server.port=8080

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

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

然后,在cn.demo.store包中,创建mapper子级包,用于存放使用MyBatis编程时的接口文件,并在mapper包中创建UserMapper接口,在接口中添加抽象方法:

/**
 * 处理用户数据的持久层接口
 */
public interface UserMapper {
  
  /**
   * 插入用户数据
   * @param user 用户数据
   * @return 受影响的行数
   */
  Integer insert(User user);
  
  /**
   * 根据用户名查询用户数据
   * @param username 用户名
   * @return 匹配的用户数据,如果没有匹配的数据,则返回null
   */
  User findByUsername(String username);

}

然后,需要在启动类的声明之前补充@MapperScan注解,以配置接口文件的位置:

@SpringBootApplication
@MapperScan("cn.demo.store.mapper")
public class StoreApplication {
  public static void main(String[] args) {
    SpringApplication.run(StoreApplication.class, args);
  }
}

src/main/resources下创建mappers文件夹,该文件夹的名称应该与复制的配置信息中保持一致!并在该文件夹中创建UserMapper.xml文件,以配置2个抽象方法的SQL映射:

  




  
    
    
    
    
    
    
  

  
  
  
    INSERT INTO t_user (
      username, password, salt, gender,
      phone, email, avatar, is_delete,
      created_user, created_time, modified_user, modified_time
    ) VALUES (
      #{username}, #{password}, #{salt}, #{gender},
      #{phone}, #{email}, #{avatar}, #{isDelete},
      #{createdUser}, #{createdTime}, #{modifiedUser}, #{modifiedTime}
    )
  
  
  
  
  


src/test/java中的cn.demo.store包中创建子级的mapper包,并在mapper包中创建UserMapperTests测试类,并在测试类的声明之前添加@RunWith(SpringRunner.class)@SpringBootTest注解:

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

}

如果使用的是SpringBoot 2.2.x系列的版本,只需要添加1个注解即可,具体使用什么样的注解,请参考默认就存在那个单元测试类。

然后,在单元测试类中编写并执行单元测试:

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

  @Autowired
  private UserMapper mapper;

  @Test
  public void insert() {
    User user = new User();
    user.setUsername("project");
    user.setPassword("1234");
    user.setSalt("salt");
    user.setGender(0);
    user.setPhone("13800138002");
    user.setEmail("[email protected]");
    user.setAvatar("avatar");
    user.setIsDelete(0);
    user.setCreatedUser("系统管理员");
    user.setCreatedTime(new Date());
    user.setModifiedUser("超级管理员");
    user.setModifiedTime(new Date());
    Integer rows = mapper.insert(user);
    System.err.println("rows=" + rows);
    System.err.println(user);
  }
  
  @Test
  public void findByUsername() {
    String username = "project";
    User result = mapper.findByUsername(username);
    System.err.println(result);
  }
}

5. 用户-注册-业务层

业务,在普通用户眼里就是“1个功能”,例如“注册”就是一个业务,在开发人员看来,它可能是由多个数据操作所组成的,例如“注册”就至少由“查询用户名对应的用户数据”和“插入用户数据”这2个数据操作组成,多个数据操作组成1个业务,在组织过程中,可能涉及一些相关的检查,及数据安全、数据完整性的保障,所以,业务层的代码主要是组织业务流程,设计业务逻辑,以保障数据的完整性和安全性

在开发领域中,数据安全指的是:数据是由开发人员所设定的规则而产生或发生变化的!

在业务层的开发中,应该先创建业务层的接口,因为,在实际项目开发中,强烈推荐“使用接口编程”的效果!

所以,先在cn.demo.store包中创建service子包,并在service包中创建UserService业务接口,并在接口中声明“注册”这个业务的抽象方法:

/**
 * 处理用户数据的业务接口
 */
public interface UserService {
  
  /**
   * 用户注册
   * @param user 客户端提交的用户数据
   */
  void reg(User user);

}

在设计抽象方法时,仅以操作成功(例如注册成功、登录成功等)为前提来设计抽象方法的返回值,涉及的操作失败将通过抛出异常来表示!

**创建异常处理:**在cn.demo.store下创建ex子包,并创建异常的父类(ServiceException)

由于需要使用异常来表示错误,所以,在实现抽象方法的功能之前,还应该先定义相关的异常,有哪些“错误”(导致操作失败的原因),就创建哪些异常类,例如,注册时,用户名可能已经被占用,则需要创建对应的异常,当用户名没有被占用,允许注册时,执行的INSERT操作也可能失败,导致相应的异常,为了便于统一管理这些异常,还应该创建自定义异常的基类,这个基类异常应该继承自RuntimeException

/**
 * 业务异常的基类
 */
public class ServiceException extends RuntimeException {

  private static final long serialVersionUID = 980104530291206274L;

  public ServiceException() {
    super();
  }

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

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

  public ServiceException(String message) {
    super(message);
  }

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

}
-----------------------------------------------------------------------------

/**
 * 用户名冲突的异常
 */
public class UsernameDuplicateException extends ServiceException {

  private static final long serialVersionUID = -1224474172375139228L;

  public UsernameDuplicateException() {
    super();
  }

  public UsernameDuplicateException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
    super(message, cause, enableSuppression, writableStackTrace);
  }

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

  public UsernameDuplicateException(String message) {
    super(message);
  }

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

}

-------------------------------------------------------------------------------
/**
 * 插入数据异常
 */
public class InsertException extends ServiceException {

  private static final long serialVersionUID = 7991875652328476596L;

  public InsertException() {
    super();
  }

  public InsertException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
    super(message, cause, enableSuppression, writableStackTrace);
  }

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

  public InsertException(String message) {
    super(message);
  }

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

}

接下来,就需要编写接口的实现类,并实现接口中的抽象方法!所以,在cn.demo.store.service包创建子级的impl包,并在impl包中创建UserServiceImpl类,实现UserService接口,在类的声明之前添加@Service注解,使得Spring框架能够创建并管理这个类的对象!并且,由于在实现过程中,必然用到持久层开发的数据操作,所以,还应该声明UserMapper对象,该对象的值应该是自动装配的:

/**
 * 处理用户数据的业务层实现类
 */
@Service
public class UserServiceImpl implements UserService {

  @Autowired
  private UserMapper userMapper;
  
  @Override
  public void reg(User user) {
    
  }

}

接下来,分析实现过程:

public void reg(User user) {
  	// 通过参数user获取尝试注册的用户名
    String username = user.getUsername();
    // 调用userMapper.findByUsername()方法执行查询
    User result = userMapper.findByUsername(username);
    // 判断查询结果是否不为null
    if (result != null) {
      // 是:查询到了数据,表示用户名已经被占用,则抛出UsernameDuplicationException
      throw new UsernameDuplicateException();
    }

    // 如果代码能执行到这一行,则表示没有查到数据,表示用户名未被注册,则允许注册
    // 创建当前时间对象:
    Date now = new Date();
    // 向参数user中补全数据:salt, password,涉及加密处理,暂不处理
    // 向参数user中补全数据:is_delete(0)
    user.setIsDelete(0);
    // 向参数user中补全数据:4项日志(now, user.getUsername())
    user.setCreaser(username);
    user.setCreatedTime(now);
    user.setModifiedUser(username);
    user.setModifiedTime(now);
    // 调用userMapper.insert()执行插入数据,并获取返回的受影响行数
    Integer rows = userMapper.insert(user);
    // 判断受影响的行数是否不为1
    if (rows != 1) {
      // 是:插入数据失败,则抛出InsertException
      throw new InsertException();
    }
}

然后,应该在src/test/java下的cn.demo.store包中创建子级的service包,并在这个包中创建UserServiceTests测试类,专门用于测试UserService接口中定义的功能:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTests {
  
  @Autowired
  private UserService service;
  
  @Test
  public void reg() {
    try {
      User user = new User();
      user.setUsername("service");
      user.setPassword("1234");
      user.setGender(0);
      user.setPhone("13800138003");
      user.setEmail("[email protected]");
      user.setAvatar("avatar");
      service.reg(user);
      System.err.println("OK.");
    } catch (ServiceException e) {
      System.err.println(e.getClass().getName());
    }
  }

}

最后,还应该处理密码加密(添加commons-codec依赖),完整业务代码例如:

/**
 * 处理用户数据的业务层实现类
 */
@Service
public class UserServiceImpl implements UserService {

  @Autowired
  private UserMapper userMapper;

  @Override
  public void reg(User user) {
    // 日志
    System.err.println("UserServiceImpl.reg()");
    // 通过参数user获取尝试注册的用户名
    String username = user.getUsername();
    // 调用userMapper.findByUsername()方法执行查询
    User result = userMapper.findByUsername(username);
    // 判断查询结果是否不为null
    if (result != null) {
      // 是:查询到了数据,表示用户名已经被占用,则抛出UsernameDuplicationException
      throw new UsernameDuplicateException();
    }

    // 如果代码能执行到这一行,则表示没有查到数据,表示用户名未被注册,则允许注册
    // 创建当前时间对象:
    Date now = new Date();
    // 向参数user中补全数据:salt, password
    String salt = UUID.randomUUID().toString();
    user.setSalt(salt);
    String md5Password = getMd5Password(user.getPassword(), salt);
    user.setPassword(md5Password);
    // 向参数user中补全数据:is_delete(0)
    user.setIsDelete(0);
    // 向参数user中补全数据:4项日志(now, user.getUsername())
    user.setCreaser(username);
    user.setCreatedTime(now);
    user.setModifiedUser(username);
    user.setModifiedTime(now);
    // 调用userMapper.insert()执行插入数据,并获取返回的受影响行数
    Integer rows = userMapper.insert(user);
    // 判断受影响的行数是否不为1
    if (rows != 1) {
      // 是:插入数据失败,则抛出InsertException
      throw new InsertException();
    }
  }
  
  /**
   * 执行密码加密,获取加密后的结果
   * @param password 原始密码
   * @param salt 盐值
   * @return 加密后的结果
   */
  private String getMd5Password(String password, String salt) {
    // 加密标准:使用salt+password+salt作为被运算数据,循环加密3次
    String result = salt + password + salt;
    for (int i = 0; i < 3; i++) {
      result = DigestUtils.md5Hex(result);
    }
    System.err.println("\tpassword=" + password);
    System.err.println("\tsalt=" + salt);
    System.err.println("\tmd5Password=" + result);
    return result;
  }
}

6. 用户-注册-控制器层

控制器层主要解决的问题是:接收客户端提交的请求,调用Service组件进行数据处理,并将处理结果响应给客户端。

先在src/main/javacn.demo.store包中创建子级util包,并在util中创建JsonResult类,用于封装响应给客户端的JSON数据中的属性:

/**
 * 封装响应JSON对象的属性的类
 * 
 * @param  响应给客户端的数据的类型(泛型)
 */
public class JsonResult {

  // 响应的标识,例如:使用200表示登录成功,使用400表示由于用户名不存在导致的登录失败
  private Integer state;
  // 操作失败/操作出错时的描述文字,例如:“登录失败,用户名不存在”
  private String message;
  // 操作成功时需要响应给客户端的数据
  private E data;

  public JsonResult() {
    super();
  }

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

  public JsonResult(Throwable e) {
    super();
    this.message = e.getMessage();
  }
 
  // 自行补充SET/GET方法
  
}

在处理请求之前,就可以直接对相关的异常进行处理(SpringMVC统一处理):

@RestControllerAdvice
public class GlobalHandleException {
  
  @ExceptionHandler(ServiceException.class)
  public JsonResult handleException(Throwable e) {
    JsonResult result = new JsonResult<>(e);
    
    if (e instanceof UsernameDuplicateException) {
      result.setState(4000);
    } else if (e instanceof InsertException) {
      result.setState(5000);
    }
    
    return result;
  }

}

src/main/javacn.demo.store包中创建子级controller包,并在controller包中创建UserController类,专门用于处理用户数据相关的请求,需要在类的声明之前添加@RestController注解,推荐在类的声明之前添加@RequestMapping("users")注解,由于需要调用Service组件处理数据,在类中还应该声明@Autowired private UserService userService;对象:

@RequestMapping("users")
@RestController
public class UserController {
  @Autowired
  private UserService userService;
}

然后,在类中添加处理请求的方法,由于异常已经被统一处理了,所以,在处理请求时,只需要以“注册成功”为前提来处理即可,不需要关心出现异常的问题:

@RequestMapping("users")
@RestController
public class UserController {

  @Autowired
  private UserService userService;
  
  /**
   * 响应到客户端的、表示操作成功的状态值
   */
  private static final int OK = 2000;
  
  // http://localhost:8080/users/reg
  @PostMapping("reg")
  public JsonResult reg(User user) {
    // 调用业务对象执行注册
    userService.reg(user);
    // 返回成功
    return new JsonResult<>(OK);
  }
  
}

完成后,启动项目,在浏览器中打开http://localhost:8080/users/reg?username=controller&password=1234即可测试。

7. 用户-注册-前端页面



8. 用户-登录-持久层

登录操作,应该是先根据用户名查询用户数据,并对查询的数据进行基本有效性的判断,后续,再验证密码,如果密码也正确,就登录成功,并返回该用户的相关信息,例如uid、username、avatar等。

在数据库的操作中,需要实现的是:根据用户名查询用户数据。该功能已经实现,则不需要再次开发。

9. 用户-登录-业务层

首先,在UserService业务层接口中添加抽象方法:

User login(String username, String password);

然后,在UserServiceImpl中实现以上方法:

@Override
public User login(String username, String password) {
    // 日志
    System.err.println("UserServiceImpl.login()");
    // 基于参数username调用userMapper.findByUsername()查询用户数据
    User result = userMapper.findByUsername(username);
    // 判断查询结果(result)是否为null
    if (result == null) {
      // 是:抛出UserNotFoundException
      throw new UserNotFoundException("登录失败,用户名不存在!");
    }

    // 判断查询结果(result)中的isDelete是否为1
    if (result.getIsDelete() == 1) {
      // 是:抛出UserNotFoundException
      throw new UserNotFoundException("登录失败,用户数据已经被删除!");
    }

    // 从查询结果(result)中获取盐值
    String salt = result.getSalt();
    // 基于参数password和盐值,调用getMd5Password()执行加密
    String md5Password = getMd5Password(password, salt);
    // 判断查询结果(result)中的密码和以上加密结果是否不一致
    if (!md5Password.equals(result.getPassword())) {
      // 是:抛出PasswordNotMatchException
      throw new PasswordNotMatchException("登录失败,密码错误!");
    }
    
    // 创建新的User对象
    User user = new User();
    // 将查询结果中的uid、username、avatar设置到新的User对象的对应的属性中
    user.setUid(result.getUid());
    user.setUsername(result.getUsername());
    user.setAvatar(result.getAvatar());
    // 返回新创建的User对象
    return user;
}

最后,在UserServiceTests中编写并执行单元测试:

@Test
public void login() {
    try {
      String username = "digests";
      String password = "0000";
      User result = service.login(username, password);
      System.err.println("OK.");
      System.err.println(result);
    } catch (ServiceException e) {
      System.err.println(e.getClass().getName());
      System.err.println(e.getMessage());
    }
}

10. 用户-登录-控制器层

由于使用了统一处理异常的做法,在这种做法中,异常的处理方式也非常简单,所以,应该优先把新的异常都处理掉,在GlobalExceptionHandler中添加更多的else if进行判断并处理即可。

为了便于向客户端响应数据,在JsonResult中添加新的构造方法:

public JsonResult(Integer state, E data) {
    super();
    this.state = state;
    this.data = data;
}

然后,在UserController中添加处理“登录”的方法:

// http://localhost:8080/users/login?username=digest&password=0000
@PostMapping("login")
public JsonResult login(String username, String password, HttpSession session) {
    // 调用userService.login()方法执行登录,并获取返回结果(成功登录的用户数据)
    User data = userService.login(username, password);
    // 将返回结果中的uid和username存入到Session
    session.setAttribute("uid", data.getUid());
    session.setAttribute("username", data.getUsername());
    // 将结果响应给客户端
    return new JsonResult<>(OK, data);
}

完成后,启动整个项目,打开浏览器,通过http://localhost:8080/users/login?username=digest&password=0000测试登录功能是否正常!

在测试过程中,可以发现,许多为null属性也在JSON结果中,这样会浪费流量,也会暴露数据的结构,应该将使得这些为null的属性不出现在JSON结果中,可以在application.properties中添加配置:

spring.jackson.default-property-inclusion=NON_NULL

11. 用户-登录-前端页面

	

12. 用户-修改密码-持久层

UserMapper接口中添加以下抽象方法:

  /**
   * 更新用户的密码
   * @param uid 用户的id
   * @param password 新的密码
   * @param modifiedUser 最后修改人
   * @param modifiedTime 最后修改时间
   * @return 受影响的行数
   */
  Integer updatePasswordByUid(
      @Param("uid") Integer uid, 
      @Param("password") String password, 
      @Param("modifiedUser") String modifiedUser, 
      @Param("modifiedTime") Date modifiedTime
  );

  /**
   * 根据用户id查询用户数据
   * @param uid 用户id
   * @return 匹配的用户数据,如果没有匹配的数据,则返回null
   */
  User findByUid(Integer uid);

然后,在UserMapper.xml中配置以上2个方法对应的SQL语句:

  
  
  
    UPDATE
      t_user
    SET
      password=#{password},
      modified_user=#{modifiedUser},
      modified_time=#{modifiedTime}
    WHERE 
      uid=#{uid}
  
  
  
  
  

最后,在UserMapperTests中编写并执行单元测试:

  @Test
  public void updatePasswordByUid() {
    Integer uid = 1;
    String password = "888888";
    String modifiedUser = "系统管理员";
    Date modifiedTime = new Date();
    Integer rows = mapper.updatePasswordByUid(uid, password, modifiedUser, modifiedTime);
    System.err.println("rows=" + rows);
  }
  
  @Test
  public void findByUid() {
    Integer uid = 1;
    User result = mapper.findByUid(uid);
    System.err.println(result);
  }

13. 用户-修改密码-业务层

由于处理业务过程中,可能会抛出异常,所以,需要先创建相关的异常类!

此次需要创建的是UpdateException异常类:

package cn.demo.store.servic.ex;

// 自行添加类的注释
public class UpdateException extends ServiceException {
  // 自行添加序列化id
  // 自行添加5个构造方法
}

在业务层接口UserService中添加抽象方法:

  /**
   * 修改密码
   * @param uid 用户的id
   * @param username 用户名
   * @param oldPassword 原密码
   * @param newPassword 新密码
   */
  void changePassword(Integer uid, String username, String oldPassword, String newPassword);

然后,在UserServiceImpl中实现以上方法:

  @Override
  public void changePassword(Integer uid, String username, String oldPassword, String newPassword) {
    System.err.println("UserServiceImpl.changePassword()");
    // 调用userMapper.findByUid()查询用户数据
    User result = userMapper.findByUid(uid);
    // 判断查询结果(result)是否为null
    if (result == null) {
      // 是:抛出UserNotFoundException
      throw new UserNotFoundException("修改密码失败,尝试访问的用户数据不存在!");
    }
    
    // 判断查询结果(result)中的isDelete属性是否为1
    if (result.getIsDelete() == 1) {
      // 是:抛出UserNotFoundException
      throw new UserNotFoundException("修改密码失败,用户数据已被删除!");
    }
      
    // 从查询结果(result)中取出盐值(salt)
    String salt = result.getSalt();
    // 基于参数oldPassword和盐值执行加密
    String oldMd5Password = getMd5Password(oldPassword, salt);
    // 判断以上加密结果与查询结果(result)中的密码是否不匹配
    if (!oldMd5Password.equals(result.getPassword())) {
      // 是:抛出PasswordNotMatchException
      throw new PasswordNotMatchException("修改密码失败,原密码错误!");
    }
    
    // 日志
    System.err.println("\t验证通过,更新密码:");
    // 基于参数newPassword和盐值执行加密
    String newMd5Password = getMd5Password(newPassword, salt);
    // 调用userMapper.updatePasswordByUid()执行更新密码(最后修改人是参数username),并获取返回值
    Integer rows = userMapper.updatePasswordByUid(uid, newMd5Password, username, new Date());
    // 判断返回结果是否不为1
    if (rows != 1) {
      // 是:抛出UpdateException
      throw new UpdateException("修改密码失败,更新密码时出现未知错误,请联系系统管理员!");
    }
  }

最后,在UserServiceTests中编写并执行单元测试:

  @Test
  public void changePassword() {
    try {
      Integer uid = 5;
      String username = "密码管理员";
      String oldPassword = "1234";
      String newPassword = "0000";
      service.changePassword(uid, username, oldPassword, newPassword);
      System.err.println("OK.");
    } catch (ServiceException e) {
      System.err.println(e.getClass().getName());
      System.err.println(e.getMessage());
    }
  }

14. 用户-修改密码-控制器层

UserController中添加处理“修改密码”请求的方法:

  @PostMapping("password/change")
  public JsonResult changePassword(String oldPassword, String newPassword, HttpSession session) {
    // 从Session中取出uid和username
    Integer uid = Integer.valueOf(session.getAttribute("uid").toString());
    String username = session.getAttribute("username").toString();
    // 调用userService.changePassword()执行修改密码
    userService.changePassword(uid, username, oldPassword, newPassword);
    // 返回操作成功
    return new JsonResult<>(OK);
  }

15. 用户-修改密码-前端页面

16. 登录拦截器

因为后续将有很多操作都是必须登录才允许访问的,如果在每个处理请求的方法中判断,工作量较大,且不利于统一管理,所以,应该通过拦截器来处理!

在SpringBoot项目中,拦截器类的写法与普通的SpringMVC项目中是相同的!所以,先在cn.demo.store包下创建子级的interceptor包,然后在interceptor包下创建LoginInterceptor,需要实现HandlerInterceptor接口,并重写preHandle()方法:

public class LoginInterceptor implements HandlerInterceptor {

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
      throws Exception {
    if (request.getSession().getAttribute("uid") == null) {
      response.sendRedirect("/web/login.html");
      return false;
    }
    return true;
  }
}

然后,还需要配置拦截器,在SpringBoot项目中,关于拦截器的配置,需要自定义配置类:

先在cn.demo.store包下创建子级的config配置包,然后在config包下创建InterceptorConfiguration类并实现WebMvcConfigurer,添加注解**@Configuration**

/**
 * 拦截器的配置类
 */
@Configuration
public class InterceptorConfiguration implements WebMvcConfigurer {

  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    HandlerInterceptor interceptor = new LoginInterceptor();
    
    List patterns = new ArrayList<>();
    patterns.add("/bootstrap3/**");
    patterns.add("/css/**");
    patterns.add("/js/**");
    patterns.add("/images/**");
    patterns.add("/web/register.html");
    patterns.add("/web/login.html");
    patterns.add("/users/reg");
    patterns.add("/users/login");
    
    registry.addInterceptor(interceptor)
      .addPathPatterns("/**")
      .excludePathPatterns(patterns);
  }
  
}

17. 用户-修改个人资料-持久层

(a) 规划需要的SQL语句

关于修改个人资料,需要解决的问题有2个:

  • 打开页面时,就显示当前登录的用户的个人资料;
  • 点击修改按钮时,执行修改个资料。

执行修改个人资料之前,需要显示当前登录的用户的个人资料,就需要事先获取当前登录的用户的个人资料,对应的SQL语句大致是:

select * from t_user where uid=?

以上查询功能已经开发,则无需重复开发。

执行修改个人资料时,需要执行的是更新数据的操作,对应的SQL语句大致是:

update t_user set phone=?, email=?, gender=?, modified_user=?, modified_time=? where uid=?

(b) 设计抽象方法

UserMapper接口中添加抽象方法:

  /**
   * 更新用户的个人资料
   * @param user 封装了用户的id和新个人资料的对象,可以更新的属性有:手机号码,电子邮箱,性别
   * @return 受影响的行数
   */
  Integer updateInfoByUid(User user);

© 配置映射并测试

UserMapper.xml中配置以上抽象方法的映射:

  
  
  
    UPDATE
      t_user
    SET
      gender=#{gender},
      phone=#{phone},
      email=#{email},
      modified_user=#{modifiedUser},
      modified_time=#{modifiedTime}
    WHERE 
      uid=#{uid}
  

UserMapperTests中编写并测试以上方法:

  @Test
  public void updateInfoByUid() {
    User user = new User();
    user.setUid(5);
    user.setPhone("13000130000");
    user.setEmail("[email protected]");
    user.setGender(0);
    Integer rows = mapper.updateInfoByUid(user);
    System.err.println("rows=" + rows);
  }

18. 用户-修改个人资料-业务层

(a) 规划业务流程、业务逻辑,并创建可能出现的异常

当需要显示个人资料时:直接查询用户的数据,进行相关的检查,完成后,就可以将数据返回了,在整个过程中,涉及的异常可能有:UserNotFoundException

当需要修改个人资料时:应该先查询用户的数据,对查询结果进行相关检查,检查无误后,则执行更新,在整个过程中,涉及的异常可能有:UserNotFoundExceptionUpdateException

(b) 设计抽象方法

UserService接口中添加抽象方法:

	/**
	 * 获取用户个人资料数据
	 * @param uid 用户id
	 * @return 返回封装了用户个人资料的User
	 */
	User getInfo(Integer uid);
	
	/**
	 * 修改用户资料
	 * @param uid 用户id
	 * @param username 用户名
	 * @param user 要修改的个人资料数据
	 */
	void changeInfo(Integer uid, String username, User user);

© 实现抽象方法并测试

UserServiceImpl类中实现以上抽象方法:

  @Override
  public User getInfo(Integer uid) {
    // 调用userMapper.findByUid()查询用户数据
    User result = userMapper.findByUid(uid);
    // 判断查询结果(result)是否为null
    if (result == null) {
      // 是:抛出UserNotFoundException
      throw new UserNotFoundException("获取用户数据失败,尝试访问的用户数据不存在!");
    }

    // 判断查询结果(result)中的isDelete属性是否为1
    if (result.getIsDelete() == 1) {
      // 是:抛出UserNotFoundException
      throw new UserNotFoundException("获取用户数据失败,用户数据已被删除!");
    }

    // 创建新的User对象
    User user = new User();
    // 通过查询结果向新User对象中封装属性:username,phone,email,gender
    user.setUsername(result.getUsername());
    user.setPhone(result.getPhone());
    user.setEmail(result.getEmail());
    user.setGender(result.getGender());
    // 返回新User对象
    return user;
  }

  @Override
  public void changeInfo(Integer uid, String username, User user) {
    // 调用userMapper.findByUid()查询用户数据
    User result = userMapper.findByUid(uid);
    // 判断查询结果(result)是否为null
    if (result == null) {
      // 是:抛出UserNotFoundException
      throw new UserNotFoundException("修改用户资料失败,尝试访问的用户数据不存在!");
    }

    // 判断查询结果(result)中的isDelete属性是否为1
    if (result.getIsDelete() == 1) {
      // 是:抛出UserNotFoundException
      throw new UserNotFoundException("修改用户资料失败,用户数据已被删除!");
    }

    // 向参数user中补充数据:uid > 参数uid
    user.setUid(uid);
    // 向参数user中补充数据:modifiedUser > 参数username
    user.setModifiedUser(username);
    // 向参数user中补充数据:modifiedTime > new Date()
    user.setModifiedTime(new Date());
    // 调用userMapper.updateInfoByUid()执行更新,并获取返回值
    Integer rows = userMapper.updateInfoByUid(user);
    // 判断返回值是否不为1
    if (rows != 1) {
      // 是:抛出UpdateException
      throw new UpdateException("修改用户资料失败,更新用户资料时出现未知错误,请联系系统管理员!");
    }
  }

完成后,在UserServiceTests中编写并执行单元测试:

  @Test
  public void getInfo() {
    try {
      Integer uid = 5;
      User result = service.getInfo(uid);
      System.err.println("OK.");
      System.err.println(result);
    } catch (ServiceException e) {
      System.err.println(e.getClass().getName());
      System.err.println(e.getMessage());
    }
  }

  @Test
  public void changeInfo() {
    try {
      Integer uid = 5;
      String username = "资料管理员";
      User user = new User();
      user.setPhone("13804380438");
      user.setEmail("[email protected]");
      user.setGender(1);
      service.changeInfo(uid, username, user);
      System.err.println("OK.");
    } catch (ServiceException e) {
      System.err.println(e.getClass().getName());
      System.err.println(e.getMessage());
    }
  }

19. 用户-修改个人资料-控制器层

(a) 处理新创建的异常类型

(b) 设计需要处理的请求

关于显示个人资料

  • 请求路径:/users/info/show
  • 请求参数:HttpSession session(严格来说,并不需要客户端提交参数,应该是从Session中获取uid即可)
  • 请求方式:GET
  • 响应数据:JsonResult

关于修改个人资料

  • 请求路径:/users/info/change
  • 请求参数:User user HttpSession session(严格来说,需要的是Session中的uid和username)
  • 请求方式:POST
  • 响应数据:JsonResult

© 处理请求并测试

UserController中添加处理请求的方法:

  // http://localhost:8080/users/info/show
  @GetMapping("info/show")
  public JsonResult showInfo(HttpSession session) {
    // 从Session中获取uid
    Integer uid = Integer.valueOf(session.getAttribute("uid").toString());
    // 调用userService.getInfo()获取数据
    User data = userService.getInfo(uid);
    // 响应成功及数据
    return new JsonResult<>(OK, data);
  }

  @PostMapping("info/change")
  public JsonResult changeInfo(User user, HttpSession session) {
    // 从Session中获取uid和username
    Integer uid = Integer.valueOf(session.getAttribute("uid").toString());
    String username = session.getAttribute("username").toString();
    // 调用userService.changeInfo()修改个人资料
    userService.changeInfo(uid, username, user);
    // 响应成功
    return new JsonResult<>(OK);
  }

20.用户-修改个人资料-前端页面

   

21. 用户-上传头像-持久层

(a) 规划需要的SQL语句

上传头像的本质是:将客户端选中并提交的文件保存到webapp下(也可以是SpringBoot项目的src/main/resources/static下),并且,在数据库中记录下该文件的路径(包含文件名),后续,当需要访问该头像时,从数据库中读取此前保存的路径,通过该路径就可以访问到头像文件。所以,上传头像时,需要执行的操作有2个:将文件保存下来,将路径记录到数据库中。

保存客户端选中并上传的文件,应该在控制器层进行处理,一般,上传技术都是由控制器技术提供的,例如,在传统的Java EE环境中,就有基于Servlet的文件上传,在处理控制器时,可以使用Struts2框架或SpringMVC框架,这些框架也都提供更加简便的文件上传的处理方式,所以,文件上传的“保存文件”操作是与控制器密切相关的,就应该由控制器层进行处理!

所以,在持久层需要处理的就只有更新数据表中用户头像字段的值!需要执行的SQL语句大致是:

update t_user set avatar=?, modifedUser=?, modifiedTime=? where uid=?

在执行更新之前,还应该检查数据的有效性(用户数据是否存在,是否被标记为删除)。

(b) 设计抽象方法

UserMapper接口中添加抽象方法:

/**
* 更新用户头像
* @param uid 用户Id
* @param avatar 头像路径
* @param modifiedUser 最后修改人
* @param modifiedTime 修改时间
* @return
*/
Integer updateAvatarByUid(
    @Param("uid") Integer uid,
    @Param("avatar") String avatar,
    @Param("modifiedUser") String modifiedUser,
    @Param("modifiedTime") Date modifiedTime
);

© 配置映射并测试

UserMapper.xml中配置以上抽象方法对应的SQL语句:

  
  
  
    UPDATE
      t_user
    SET
      avatar=#{avatar},
      modified_user=#{modifiedUser},
      modified_time=#{modifiedTime}
    WHERE 
      uid=#{uid}
  

最后,在UserMapperTests中编写并执行单元测试:

  @Test
  public void updateAvatarByUid() {
    Integer uid = 6;
    String avatar = "头像路径";
    String modifiedUser = "头像管理员";
    Date modifiedTime = new Date();
    Integer rows = mapper.updateAvatarByUid(uid, avatar, modifiedUser, modifiedTime);
    System.err.println("rows=" + rows);
  }

22. 用户-上传头像-业务层

(a) 规划业务流程、业务逻辑,并创建可能出现的异常

在业务层处理上传头像时,依然是先检查用户数据的有效性,检查完成后,允许执行更新头像。

(b) 设计抽象方法

UserService接口中添加抽象方法:

void changeAvatar(Integer uid, String username, String avatar);

© 实现抽象方法并测试

UserServiceImpl类中实现以上抽象方法:

具体代码为:

  @Override
  public void changeAvatar(Integer uid, String username, String avatar) {
    // 调用userMapper.findByUid()查询用户数据
    User result = userMapper.findByUid(uid);
    // 判断查询结果(result)是否为null
    if (result == null) {
      // 是:抛出UserNotFoundException
      throw new UserNotFoundException("修改用户头像失败,尝试访问的用户数据不存在!");
    }

    // 判断查询结果(result)中的isDelete属性是否为1
    if (result.getIsDelete() == 1) {
      // 是:抛出UserNotFoundException
      throw new UserNotFoundException("修改用户头像失败,用户数据已被删除!");
    }

    // 调用userMapper.updateAvatarByUid()执行更新,并获取返回值
    Integer rows = userMapper.updateAvatarByUid(uid, avatar, username, new Date());
    // 判断返回值是否不为1
    if (rows != 1) {
      // 是:抛出UpdateException
      throw new UpdateException("修改用户头像失败,更新头像时出现未知错误,请联系系统管理员!");
    }
  }

最后,在UserServiceTests中编写并执行单元测试:

  @Test
  public void changeAvatar() {
    try {
      Integer uid = 5;
      String username = "管理员";
      String avatar = "1234";
      service.changeAvatar(uid, username, avatar);
      System.err.println("OK.");
    } catch (ServiceException e) {
      System.err.println(e.getClass().getName());
      System.err.println(e.getMessage());
    }
  }

23. 用户-上传头像-控制器层

关于MultipartFile的API

在处理上传时,主要使用MultipartFile表示客户端上传的文件,这种MutlipartFile是服务器端对客户端上传的文件数据进行封装了的对象,它不仅仅只是文件数据而已,还封装了与文件数据相关的其它数据,可以通过相关API获取这些数据,常用的MultipartFile的API有:

  • String getOriginalFilename():获取上传的文件的原始名称,即这个文件在客户端设备中的名称;
  • boolean isEmpty():判断上传的文件是否为空,如果在上传的表单中没有选择文件,或选择的文件是0字节的,则返回true,否则,返回false
  • long getSize():获取客户端上传的文件的大小,以字节为单位;
  • String getContentType():获取客户端上传的文件的MIME类型,是根据文件的扩展名得到的;
  • void transferTo(File dest):执行保存客户端上传的文件,参数就是保存到的位置。

创建上传文件时的异常

在执行上传时,如果上传的文件为空,或上传的文件大小超标,或上传的文件类型不符,都应该抛出对应的异常,这些异常都是在控制器中处理上传时出现的,并不是处理业务过程中出现的,所以,不应该继承自原有的ServiceException,应该为这些异常创建新的FileUploadException基类,该基类是继承自RuntimeException的,而对应某种具体错误的异常都应该继承自这个基类异常!

另外,在保存上传的文件时(调用MultipartFile对象的transferTo()方法),会抛出2种异常,在具体处理时,也应该捕获这2种异常,并在捕获后抛出对应的FileStateExceptionFileUploadIOException,这2个自定义异常也应该继承自FileUploadException

所以,应该先创建相关的异常类,它们应该是:

package cn.demo.store.controller.ex;

public class FileUploadException extends RuntimeException {
  // 生成序列化版本id
  // 生成5个构造方法
}
----------------------------------------------------
public class FileEmptyException extends FileUploadException {
  // 生成序列化版本id
  // 生成5个构造方法
}
----------------------------------------------------
public class FileSizeException extends FileUploadException {
  // 生成序列化版本id
  // 生成5个构造方法
}
----------------------------------------------------
public class FileTypeException extends FileUploadException {
  // 生成序列化版本id
  // 生成5个构造方法
}
----------------------------------------------------
public class FileStateException extends FileUploadException {
  // 生成序列化版本id
  // 生成5个构造方法
}
----------------------------------------------------
public class FileUploadIOException extends FileUploadException {
  // 生成序列化版本id
  // 生成5个构造方法
}

当创建了以上异常类之后,应该在GlobalHandleException的统一处理异常的过程中,对以上异常进行处理!

首先,原有处理异常的方法添加了@ExceptionHandler(ServiceException.class),则表示该方法只处理ServiceException及其子孙类异常,而以上创建的异常类与ServiceException并没有继承关系,将不会被处理,所以,需要先修改注解参数:

@ExceptionHandler({ServiceException.class, FileUploadException.class})

则以上注解对应的方法可以处理ServiceExceptionFileUploadException这2大类异常!

然后,在处理过程中,添加else if分支进行判断并处理即可!


配置上传的限制值

application.properties中配置上传的文件大小、文件类型的限制:

project.avatar-max-size=112640 # 自定义名称
project.avatar-types=image/png,image/jpeg,image/gif

如果是基本值(数值、字符串、布尔值),在配置属性时,等于号的右侧直接写值就可以,如果是List类型的,则各个值之间使用逗号分隔,如果是数组类型的,则使用相同的属性名加上[0]类似的下标,配置多行属性。

后续,在程序中,在全局属性之前通过@Value(${属性名})即可读取以上的配置值。

在控制器中读取配置

在控制器类中,声明全局属性,在属性的声明之前添加@Value注解,以读取以上自定义配置:

  /**
   * 上传头像时,允许使用的文件的最大大小,使用字节为单位
   */
  @Value("${project.avatar-max-size}")
  private int avatarMaxSize;
  
  /**
   * 上传头像时,允许使用的头像文件的MIME类型
   */
  @Value("${project.avatar-types}")
  private List avatarTypes;

处理上传的请求

在控制器类中,添加处理请求的方法:

	/**
	 * 上传头像文件时的最大大小,使用字节为单位(从配置文件读取)
	 */
	@Value("${project.avatar-max-size}")
	private int avatarMaxSize;
	/**
	 * 上传头像时允许的图片类型(从配置文件中读取)
	 */
	@Value("${project.avatar-types}")
	private List<String> avatarTypes;
	
	@PostMapping("avatar/change")
	public JsonResult<String> changeAvatar(MultipartFile file,HttpSession session){
     
		System.err.println("UserController.changeAvatar()");
		// 判断上传文件是否为空
		boolean isEmpty = file.isEmpty();
		if(isEmpty) {
     
			throw new FileEmptyException("上传文件失败!请选择有效的头像文件!");
		}
		// 上传文件的大小(SpringBoot框架默认限制了上传文件的大小)
		long size = file.getSize();
		if(size > avatarMaxSize) {
     
			throw new FileSizeException("上传文件失败,不允许上传超过"+(avatarMaxSize/1024)+"KB大小的图片文件");
		}
		// 上传文件的类型
		String contentType = file.getContentType();
		if(!avatarTypes.contains(contentType)) {
     
			throw new FileTypeException("上传头像失败,只允许上传如下格式:\n\n"+ avatarTypes);
		}
		// 创建保存头像文件的目录(需要的话也可以写在properties配置文件中)
		String dirName = "upload";
		// 获取webapp下的某个文件夹的真实路径,"upload"是要创建的子目录名称
		String parentPath = session.getServletContext().getRealPath(dirName);
		
		// 目标文件夹
		File parent = new File(parentPath);
		if(!parent.exists()) {
     
			parent.mkdirs();
		}
		
		// 上传的文件保存的文件名(用当前时间表示,防止重复)
		String filename = ""+System.currentTimeMillis()+System.nanoTime();
		// 上传的文件的原始名
		String originalFilename = file.getOriginalFilename();
		// 上传的文件保存的后缀名
        /* 如果原文件名中没有小数点,则返回-1,在这种情况下,还调用substring截取,就会出现StringIndexOutOfBoundsException
   		 如果原文件名中只有1个小数点,且是文件名的第1个字符,这样的命名方式其实是表示Linux系统中的隐藏文件,且substring是不合理的
   		 可能需要进行 if (beginIndex > 0) 的判断
   		 (以上判断因为在上面对上传文件的类型做了处理,所以得到的都是正确的文件格式,以上判断就不需要了)
   		 */
        
		String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
		// 文件名称
		String child = filename + suffix;
		
		// 上传的文件保存的路径及名字
		File dest = new File(parent, child);
		// 执行保存文件
		try {
     
			file.transferTo(dest );
		} catch (IllegalStateException e) {
     
			throw new FileStateException("上传文件失败!原文件可能被删除, 请稍后尝试!");
		} catch (IOException e) {
     
			throw new FileUploadIOException("上传文件失败!原文件读写出错,请稍后尝试");
		}
		
		// 将上传的文件路径保存到数据库中
		Integer uid = Integer.valueOf(session.getAttribute("uid").toString());
		String username = session.getAttribute("username").toString();
		String avatar = "/"+ dirName +"/" + child;
		userService.changeAvatar(uid, username, avatar );
		
		// 响应成功与头像路径
		return new JsonResult<>(OK, avatar);
	}

自定义全局的上传大小限制

在启动类中(StoreApplication)添加方法进行配置:

	/**
	 * 获取MultipartConfigElement
	 * 添加了@Bean注解的方法,会被spring框架调用并管理返回的类型
	 * @return MultipartConfigElement类型对象,是上传文件的配置类型的对象
	 */
	@Bean
	public MultipartConfigElement getMultipartConfigElement() {
		MultipartConfigFactory factory = new MultipartConfigFactory();
		// 关于文件上传的全局配置
		// 头像500KB,买家秀图片1MB,买家秀视频5MB
		// 上传的文件的最大大小
		factory.setMaxFileSize(DataSize.ofMegabytes(5));
		// 请求的数据量的最大大小(请求数据量包含文件大小)
		factory.setMaxRequestSize(DataSize.ofMegabytes(5));
		
		return factory.createMultipartConfig();
	}

24. 用户-上传头像-前端页面

	<script type="text/javascript">
		// 文档加载完执行的操作
		$(document).ready(function(){
     
			var avatar = $.cookie("avatar");
			if(avatar != null){
     
				$("#img-avatar").attr("src",avatar);
			}
		});
		
		$("#btn-change-avatar").click(function(){
     
			$.ajax({
     
				"url":"/users/avatar/change",
				"data":new FormData($("#form-change-avatar")[0]),
				"contentType":false,
				"processData":false,
				"type":"post",
				"dateType":"json",
				"success":function(json){
     
					if(json.state==200){
     
						alert("修改头像成功!"+json.date);
						// 显示新头像
						$("#img-avatar").attr("src",json.date);
						// 把新头像路径更新到cookie中
						$.cookie("avatar", json.date, {
     "expires":7});
					}else{
     
						alert(json.message);
					}
				},
				"error":function(){
     
					alert("您的登录信息已过期,请重新登录!");
				}
			});
		});
	</script>

显示头像的逻辑应该是:

  • 登录成功后,就把用户的头像从服务器端响应到客户端,并由客户端把头像路径记录下来(可把头像路径存储到cookie中);
  • 每次需要显示头像时(例如打开上传头像页面时),从此前的记录中读取头像并显示;
  • 当成功的上传了新头像后,更新本地记录的头像路径(更新cookie)。

接下来,应该在登录成功之后,将得到的头像数据保存在Cookie中!

如果需要使用Cookie,可以通过jQuery中的$.cookie()函数来实现,如果需要向Cookie中存入数据,其语法格式是:

$.cookie(名称, 值, {"expires": 有效多少天});

如果需要读取Cookie中的已经保存的数据,语法格式是:

var 值 = $.cookie(名称);
alert("登录成功!");
if(json.date.avatar == undefined){
     
    $.cookie("avatar", null, {
     "expires":7});
}else{
     
    // 将头像路径保存进cookie
    $.cookie("avatar", json.date.avatar, {
     "expires":7});
}

然后,在需要显示头像的页面,例如upload.html中,需要先检查是否引用了使用jQuery中的Cookie的文件,默认在upload.html中并没有引用该文件,所以,需要先补充:


当页面刚刚加载时,就直接读取Cookie中保存的头像信息,并显示在``标签中:

$(document).ready(function() {
  var avatar = $.cookie("avatar");
  if (avatar != null) {
    $("#img-avatar").attr("src", avatar);
  }
});

最后,当上传成功后,还应该把新路径更新到Cookie中:

$.cookie("avatar", json.data, {"expires":7});

附:在SpringMVC中统一处理异常

SpringMVC允许使用某个方法处理多种不同的异常,该方法的声明应该是:

  • 应该使用public权限;
  • 返回值类型的选取原则可以参考处理请求的方法;
  • 方法名称可以自定义;
  • 方法的参数列表中必须至少包含1个异常类型的参数,且该异常类型必须是所需要处理的所有异常的父类;
  • 必须添加@ExceptionHandler注解。

例如:

@ExceptionHandler
public JsonResult handleException(Throwable e) {
    JsonResult result = new JsonResult();
    if (e instanceof UsernameDuplicateException) {
      result.setState(2);
      result.setMessage("【ExceptionHandler】注册失败,用户名已经被占用!");
    } else if (e instanceof InsertException) {
      result.setState(3);
      result.setMessage("【ExceptionHandler】注册失败,保存注册数据时出现未知错误,请联系系统管理员!");
    }
    return result;
}

但是,并不是所有的异常都应该这样来处理,例如NullPointerExceptionClassCastException这些都不应该这样处理,这个方法中也不可能穷举所有的异常,所以,还可以在@ExceptionHandler注解中添加参数的配置,关于@ExceptionHandler注解的源代码:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {

  /**
   * Exceptions handled by the annotated method. If empty, will default to any
   * exceptions listed in the method argument list.
   */
  Class[] value() default {};

}

则使用注解时,应该配置为:

@ExceptionHandler(ServiceException.class)

所以,在创建自定义异常时,应该给自定义的异常创建公共的父类(基类),便于统一表示这些自定义异常。

创建的自定义异常也应该是RuntimeException的子孙类,则,调用可能抛出异常的方法时,不必强制在语法中进行try...catchthrows

关于这种统一处理异常的方法,只能作用于当前控制器类中,如果处理异常的代码并不在当前类中,可以:

  • 把处理异常的方法放在控制器类的基类中,则每个实际使用的控制器通过继承的方式都可以有这个方法;
  • 在处理异常的方法所在的类的声明之前,添加@ControllerAdvice@RestControllerAdvice注解,这2个注解在普通的SpringMVC项目中默认是不识别的,需要自行配置,在SpringBoot项目中可以直接使用。

所以,最终,统一处理异常的代码是:

@RestControllerAdvice
public class GlobalHandleException {
  
  @ExceptionHandler(ServiceException.class)
  public JsonResult handleException(Throwable e) {
    JsonResult result = new JsonResult();
    
    if (e instanceof UsernameDuplicateException) {
      result.setState(2);
      result.setMessage("注册失败,用户名已经被占用!");
    } else if (e instanceof InsertException) {
      result.setState(3);
      result.setMessage("注册失败,保存注册数据时出现未知错误,请联系系统管理员!");
    }
    
    return result;
  }

}

你可能感兴趣的:(Java,java,spring,boot,项目)