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映射:

<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE mapper PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"      
 "http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">

<mapper namespace="cn.demo.store.mapper.UserMapper">

  <resultMap type="cn.demo.store.entity.User" id="UserEntityMap">
    <id column="uid" property="uid"/>
    <result column="is_delete" property="isDelete"/>
    <result column="created_user" property="createdUser"/>
    <result column="created_time" property="createdTime"/>
    <result column="modified_user" property="modifiedUser"/>
    <result column="modified_time" property="modifiedTime"/>
  </resultMap>

  <!-- 插入用戶數據 -->
  <!-- Integer insert(User user); -->
  <insert id="insert" useGeneratedKeys="true" keyProperty="uid">
    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}
    )
  </insert>
  
  <!-- 根據用戶名查詢用戶數據 -->
  <!-- User findByUsername(String username) -->
  <select id="findByUsername" resultMap="UserEntityMap">
    SELECT * FROM t_user WHERE username=#{username}
  </select>

</mapper>

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 <E> 響應給客戶端的數據的類型(泛型)
 */
public class JsonResult<E> {

  // 響應的標識,例如:使用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<Void> handleException(Throwable e) {
    JsonResult<Void> 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<Void> reg(User user) {
    // 調用業務對象執行註冊
    userService.reg(user);
    // 返回成功
    return new JsonResult<>(OK);
  }
  
}

完成後,啓動項目,在瀏覽器中打開http://localhost:8080/users/reg?username=controller&password=1234即可測試。

7. 用戶-註冊-前端頁面

<script type="text/javascript">
		$("#btn-reg").click(function(){
			// 前端對數據進行校驗
			$.ajax({
				"url":"/users/reg",
				"data":$("#form-reg").serialize(),
				"type":"post",
				"dateType":"json",
				"success":function(json){
					if(json.state==2000){
						alert("註冊成功!");
						// location.herf("login.html");
					}else{
						alert(json.message);
					}
				}
			});
		});
</script>

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<User> 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. 用戶-登錄-前端頁面

	<script type="text/javascript">
		$("#btn-login").click(function(){
			// 前端對數據進行校驗
			$.ajax({
				"url":"/users/login",
				"data":$("#form-login").serialize(),
				"type":"post",
				"dateType":"json",
				"success":function(json){
					if(json.state==200){
						alert("登錄成功!");
					}else{
						alert(json.message);
					}
				}
			});
		});
			
	</script>

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語句:

  <!-- 更新用戶的密碼 -->
  <!-- Integer updatePasswordByUid(
      @Param("uid") Integer uid, 
      @Param("password") String password, 
      @Param("modifiedUser") String modifiedUser, 
      @Param("modifiedTime") Date modifiedTime
  ) -->
  <update id="updatePasswordByUid">
    UPDATE
      t_user
    SET
      password=#{password},
      modified_user=#{modifiedUser},
      modified_time=#{modifiedTime}
    WHERE 
      uid=#{uid}
  </update>
  
  <!-- 根據用戶id查詢用戶數據 -->
  <!-- User findByUid(Integer uid) -->
  <select id="findByUid" resultMap="UserEntityMap">
    SELECT * FROM t_user WHERE uid=#{uid}
  </select>

最後,在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<Void> 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<String> 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中配置以上抽象方法的映射:

  <!-- 更新用戶的個人資料 -->
  <!-- Integer updateInfoByUid(User user) -->
  <update id="updateInfoByUid">
    UPDATE
      t_user
    SET
      gender=#{gender},
      phone=#{phone},
      email=#{email},
      modified_user=#{modifiedUser},
      modified_time=#{modifiedTime}
    WHERE 
      uid=#{uid}
  </update>

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<User> 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<Void> 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.用戶-修改個人資料-前端頁面

   <script type="text/javascript">
   	// 文檔加載完成觸發的事件
   	$(document).ready(function(){
   		// 前端對數據進行校驗
   		$.ajax({
   			"url":"/users/info/show",
   			"type":"get",
   			"dateType":"json",
   			"success":function(json){
   				if(json.state==200){
   					$("#username").val(json.data.username);
   					$("#phone").val(json.data.phone);
   					$("#email").val(json.data.email);
   					var radio = json.data.gender == 0 ? $("gender-female") : $("gender-male");
   					radio.prop("checked","checked");
   				}else{
   					alert(json.message);
   				}
   			}
   		});
   	});
   	
   	$("#btn-change-info").click(function(){
   		// 前端對數據進行校驗
   		$.ajax({
   			"url":"/users/info/change",
   			"data":$("#form-change-info").serialize(),
   			"type":"post",
   			"dateType":"json",
   			"success":function(json){
   				if(json.state==200){
   					alert("修改個人資料成功!");
   				}else{
   					alert(json.message);
   				}
   			},
   			"error":function(){
   				alert("您的登錄信息已過期,請重新登錄!");
   			}
   		});
   	});
   </script>

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語句:

  <!-- 更新用戶的頭像 -->
  <!-- Integer updateAvatarByUid(
      @Param("uid") Integer uid,
      @Param("avatar") String avatar,
      @Param("modifiedUser") String modifiedUser,
      @Param("modifiedTime") Date modifiedTime
  ); -->
  <update id="updateAvatarByUid">
    UPDATE
      t_user
    SET
      avatar=#{avatar},
      modified_user=#{modifiedUser},
      modified_time=#{modifiedTime}
    WHERE 
      uid=#{uid}
  </update>

最後,在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<String> 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中並沒有引用該文件,所以,需要先補充:

<script src="../bootstrap3/js/jquery.cookie.js" type="text/javascript" charset="utf-8"></script>

當頁面剛剛加載時,就直接讀取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<Void> handleException(Throwable e) {
    JsonResult<Void> result = new JsonResult<Void>();
    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<? extends Throwable>[] value() default {};

}

則使用註解時,應該配置爲:

@ExceptionHandler(ServiceException.class)

所以,在創建自定義異常時,應該給自定義的異常創建公共的父類(基類),便於統一表示這些自定義異常。

創建的自定義異常也應該是RuntimeException的子孫類,則,調用可能拋出異常的方法時,不必強制在語法中進行try...catchthrows

關於這種統一處理異常的方法,只能作用於當前控制器類中,如果處理異常的代碼並不在當前類中,可以:

  • 把處理異常的方法放在控制器類的基類中,則每個實際使用的控制器通過繼承的方式都可以有這個方法;
  • 在處理異常的方法所在的類的聲明之前,添加@ControllerAdvice@RestControllerAdvice註解,這2個註解在普通的SpringMVC項目中默認是不識別的,需要自行配置,在SpringBoot項目中可以直接使用。

所以,最終,統一處理異常的代碼是:

@RestControllerAdvice
public class GlobalHandleException {
  
  @ExceptionHandler(ServiceException.class)
  public JsonResult<Void> handleException(Throwable e) {
    JsonResult<Void> result = new JsonResult<Void>();
    
    if (e instanceof UsernameDuplicateException) {
      result.setState(2);
      result.setMessage("註冊失敗,用戶名已經被佔用!");
    } else if (e instanceof InsertException) {
      result.setState(3);
      result.setMessage("註冊失敗,保存註冊數據時出現未知錯誤,請聯繫系統管理員!");
    }
    
    return result;
  }

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章