[springboot 開發單體web shop] 5. 使用者登入及首頁展示
目錄
- 使用者登入
- 實現service
- 實現Controller
- 使用者登出
- 開發除錯小福利
- java日誌追蹤
- sql日誌追蹤
- 原始碼下載
- 下節預告
使用者登入
在之前的文章中我們實現了使用者註冊和驗證功能,接下來我們繼續實現它的登入,以及登入成功之後要在頁面上顯示的資訊。
接下來,我們來編寫程式碼。
實現service
在com.liferunner.service.IUserService
public interface IUserService {
...
/**
* 使用者登入
* @param userRequestDTO 請求dto
* @return 登入使用者資訊
* @throws Exception
*/
Users userLogin(UserRequestDTO userRequestDTO) throws Exception;
}
然後,在com.liferunner.service.impl.UserServiceImpl
實現類中實現:
@Service @Slf4j public class UserServiceImpl implements IUserService { ... @Override public Users userLogin(UserRequestDTO userRequestDTO) throws Exception { log.info("======使用者登入請求:{}", userRequestDTO); Example example = new Example(Users.class); val condition = example.createCriteria(); condition.andEqualTo("username", userRequestDTO.getUsername()); condition.andEqualTo("password", MD5GeneratorTools.getMD5Str(userRequestDTO.getPassword())); val user = this.usersMapper.selectOneByExample(example); log.info("======使用者登入處理結果:{}", user); return user; } }
Error Tips:
這裡有一個小小的坑點
,大家一定要注意,在使用selectOneByExample()
查詢的時候,該方法傳入的引數一定注意是tk.mybatis.mapper.entity.Example
例項,而不是tk.mybatis.mapper.entity.Example.Criteria
,否則會報動態SQL生成查詢錯誤,資訊如下:
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'distinct' in 'class tk.mybatis.mapper.entity.Example$Criteria' at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:92) at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:440) at com.sun.proxy.$Proxy106.selectOne(Unknown Source) at org.mybatis.spring.SqlSessionTemplate.selectOne(SqlSessionTemplate.java:159) at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:87) at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:93) at com.sun.proxy.$Proxy109.selectOneByExample(Unknown Source) at com.liferunner.service.impl.UserServiceImpl.userLogin(UserServiceImpl.java:80) ...
新人在寫程式碼的時候,特別容易在上一行寫了查詢變數,下一行就直接開用了,越是簡單的錯誤越是讓人無從下手。
實現Controller
@RestController
@RequestMapping(value = "/users")
@Slf4j
@Api(tags = "使用者管理")
public class UserController {
...
@ApiOperation(value = "使用者登入", notes = "使用者登入介面")
@PostMapping("/login")
public JsonResponse userLogin(@RequestBody UserRequestDTO userRequestDTO,
HttpServletRequest request,
HttpServletResponse response) {
try {
if (StringUtils.isBlank(userRequestDTO.getUsername()))
return JsonResponse.errorMsg("使用者名稱不能為空");
if (StringUtils.isBlank(userRequestDTO.getPassword()) ||
userRequestDTO.getPassword().length() < 8) {
return JsonResponse.errorMsg("密碼為空或長度小於8位");
}
val user = this.userService.userLogin(userRequestDTO);
UserResponseDTO userResponseDTO = new UserResponseDTO();
BeanUtils.copyProperties(user, userResponseDTO);
log.info("BeanUtils copy object {}", userResponseDTO);
if (null != userResponseDTO) {
// 設定前端儲存的cookie資訊
CookieTools.setCookie(request, response, "user",
JSON.toJSONString(userResponseDTO), true);
return JsonResponse.ok(userResponseDTO);
}
} catch (Exception e) {
e.printStackTrace();
log.error("使用者登入失敗,{},exception = {}", userRequestDTO, e.getMessage());
}
return JsonResponse.errorMsg("使用者登入失敗");
}
}
在上面的程式碼中,基本校驗問題就不再贅述,我們主要關注幾點新的特性資訊:
com.liferunner.dto.UserResponseDTO
將我們需要展示給前端的資料封裝為一個新的返回物件,我們從資料庫中查詢出來的Users
pojo包含使用者的所有資料,比如其中的password
、mobile
等等一些使用者私密的資料是不應該展示給前端的,即便要展示,那也是需要經過脫敏以及加密。因此,常見的做法就是封裝一個新的返回物件,其中只需要包含前端需要的資料欄位就可以了。
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ApiModel(value = "使用者資訊返回DTO", description = "使用者登入成功後需要的返回物件")
public class UserResponseDTO {
/**
* 主鍵id
*/
private String id;
/**
* 使用者名稱
*/
private String username;
/**
* 暱稱 暱稱
*/
private String nickname;
/**
* 頭像
*/
private String face;
/**
* 性別 1:男 0:女 2:保密
*/
private Integer sex;
}
在這裡建議大家使用Ctrl+C
我們的com.liferunner.pojo.Users
物件,然後刪除掉我們不需要的欄位就可以了,為什麼這麼建議
呢,是因為下一個好處啦。
org.springframework.beans.BeanUtils.copyProperties(user, userResponseDTO);
大家可以看到,這裡直接使用的是Spring BeanUtils
工具類進行的值拷貝,就減少了我們迴圈遍歷每一個欄位去挨個賦值(SetValue)
的工作。(也是一種偷懶小技巧哦,這樣是不對的~)CookieTools.setCookie();
之前我們有提過,一般情況下,我們使用者登入之後,資料都會被儲存在本地瀏覽器Cookie
中,比如我登入的baidu.com
:
此時,滑鼠在圖片中左側的Cookies => www.baidu.com
右鍵clear
,然後再次重新整理我們當前介面,效果如下:
我們可以看到,從登入狀態已經變為退出狀態了,並且Cookies
中的內容也少了很多,這就說明,百度是把我們的使用者登入資訊加密後儲存在了瀏覽器cookie中。
大家可以檢視京東,淘寶等等,也是基於這種方式實現的,開篇之初就說過,我們的系統是基於生產來實現的demo,那麼我們就是用主流的實現方法來做。當然,有的同學會說,這個應該我們把資料傳遞給前端,讓前端來實現的!!!當然,你說的對,可是我們掌握一種實現方式,對於我們個人而言應該是沒有壞處的吧?
這裡就需要一個工具類了,大家可以在github傳送門來下載相關程式碼。目錄com.liferunner.utils.CookieTools
.com.alibaba.fastjson.JSON.toJSONString(userResponseDTO)
因為我們要返回的是一個物件,但是cookie
中我們需要放入的是String
,這裡我們引入了alibaba的JSON工具,在mscx-shop-common/pom.xml
,加入依賴:<dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.56</version> </dependency> </dependencies>
使用者登出
在使用者操作結束之後,我們需要將使用者從系統中退出登入,因為我們的使用者登入資訊會儲存在瀏覽器cookie中,因此,我們需要根據使用者的登出操作來刪除相關使用者快取:
@ApiOperation(value = "使用者登出",notes = "使用者登出",httpMethod = "POST")
@PostMapping("/logout")
public JsonResponse userLogout(@RequestParam String uid,
HttpServletRequest request,HttpServletResponse response){
// clear front's user cookies
CookieTools.deleteCookie(request,response,"user");
// return operational result
return JsonResponse.ok();
}
開發除錯小福利
java日誌追蹤
一般在電商場景中,對於請求的響應時間有著極其嚴格的要求,比如你在一個網站買商品的時候,如果每點選一次按鈕都要等待,或者系統感覺卡頓一下,你會毫不猶豫的選擇右上角的小紅叉
,把它幹掉。因此,在我們系統的開發過程中,很多時候需要對我們的請求響應時間進行監控,甚至會通過壓力測試來進行測試。但是,讓我們在每一個方法中都做這種請求的實現,顯然是不合理甚至說是讓開發人員難受的,所以,我們來實現一種通用的做法,那就是通過AOP
,面向切面來實現。關於切面的基本使用,大家可以參考AOP傳送門,接下來,開始我們的編碼。
根據springboot
實現功能三部曲:
setp 1. 新增依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
step 2. 啟動配置(沒有就忽略掉這一步)
setp 3. 加註解
在我們的mscx-shop-api
專案中,建立com.liferunner.api.aspect
package,然後建立com.liferunner.api.aspect.CommonLogAspect
,程式碼如下:
package com.liferunner.api.aspect;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* CommonLogAspect for : AOP切面實現日誌確認
*
* @author <a href="mailto:[email protected]">Isaac.Zhang | 若初</a>
* @since 2019/11/11
*/
@Component
@Aspect
@Slf4j
public class CommonLogAspect {
@Around("execution(* com.liferunner.api.controller..*.*(..))")
public void recordLogTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("----------- {}.{} process log time started.---------------",
proceedingJoinPoint.getTarget().getClass(),
proceedingJoinPoint.getSignature().getName());
val startTime = System.currentTimeMillis();
proceedingJoinPoint.proceed();
val afterTime = System.currentTimeMillis();
if (afterTime - startTime > 1000) {
log.warn("cost : {}", afterTime - startTime);
} else {
log.info("cost : {}", afterTime - startTime);
}
log.info("----------- {}.{} process log time ended.---------------",
proceedingJoinPoint.getSourceLocation().getClass(),
proceedingJoinPoint.getSignature().getName());
}
}
- 第一行日誌代表我們想要監控的是哪個類的哪個方法
proceedingJoinPoint.proceed();
表示方法執行- 當請求查過1000ms的時候,我們使用
log.warn(...)
進行日誌告警
step 4. 效果演示
- 查詢使用者耗時
- 註冊使用者耗時
從上圖,我們明顯能看出來我們每一次的請求耗時,之後就可以針對性的對每一個方法進行優化!!!
sql日誌追蹤
在我們開發的過程中,往往會遇到針對資料庫的CRUD
的操作,但是,因為我們使用了mybatis
動態生成了簡單的SQL查詢,而不是手動編寫的,比如我們在UserServiceImpl.java
中實現的使用者查詢以及使用者註冊程式碼中的tk.mybatis.mapper.entity.Example
以及 this.usersMapper.insertSelective(user);
public Users findUserByUserName(String username) {
// 構建查詢條件
Example example = new Example(Users.class);
val condition = example.createCriteria()
.andEqualTo("username", username);
return this.usersMapper.selectOneByExample(example);
}
@Transactional(propagation = Propagation.REQUIRED)
@Override
public Users createUser(UserRequestDTO userRequestDTO) throws Exception {
log.info("======begin create user : {}=======", userRequestDTO);
val user = Users.builder()
.id(sid.next()) //生成分散式id
.username(userRequestDTO.getUsername())
.password(MD5GeneratorTools.getMD5Str(userRequestDTO.getPassword()))
.birthday(DateUtils.parseDate("1970-01-01", "yyyy-MM-dd"))
.nickname(userRequestDTO.getUsername())
.face(this.FACE_IMG)
.sex(SexEnum.secret.type)
.createdTime(new Date())
.updatedTime(new Date())
.build();
this.usersMapper.insertSelective(user);
log.info("======end create user : {}=======", userRequestDTO);
return user;
}
一旦遇到了問題之後,我們往往不知道到底是哪裡出現了錯誤,這個時候我們的SQL
是否有問題我們也不知道,因此,接下來我們來配置一種可以讓我們看
到SQL的小實現:
1.設定日誌配置(如圖)
2.修改mybatis配置(log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
)
3.SELECT
效果演示
4.INSERT
效果演示
從上圖可以看出控制檯JDBC操作進行了2次,其實第一次是對我們的使用者名稱進行校驗。第二次INSERT
是真實的插入。
通過上面的演示結果,大家可以想到,這個日誌針在我們日常的開發中解決問題是非常有必要的。但是一定記得,在上生產的時候,日誌一定要關閉,否則資料量一旦大了之後,會對系統的效能造成嚴重傷害!!!
原始碼下載
Github 傳送門
Gitee 傳送門
下節預告
下一節我們將繼續開發我們電商的核心部分-商品以及廣告的展示,在過程中使用到的任何開發元件,我都會通過專門的一節來進行介紹的,兄弟們末慌!
gogog