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 Framework和
MySQL 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/java的cn.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/java的cn.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
;
當需要修改個人資料時:應該先查詢用戶的數據,對查詢結果進行相關檢查,檢查無誤後,則執行更新,在整個過程中,涉及的異常可能有:UserNotFoundException
,UpdateException
。
(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種異常,並在捕獲後拋出對應的FileStateException
和FileUploadIOException
,這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})
則以上註解對應的方法可以處理ServiceException
和FileUploadException
這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;
}
但是,並不是所有的異常都應該這樣來處理,例如NullPointerException
、ClassCastException
這些都不應該這樣處理,這個方法中也不可能窮舉所有的異常,所以,還可以在@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...catch
或throws
。
關於這種統一處理異常的方法,只能作用於當前控制器類中,如果處理異常的代碼並不在當前類中,可以:
- 把處理異常的方法放在控制器類的基類中,則每個實際使用的控制器通過繼承的方式都可以有這個方法;
- 在處理異常的方法所在的類的聲明之前,添加
@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;
}
}