前后端分離Controller層開發 筆記
如要開發如下用戶接口:
1.登錄
POST /user/login
request
Content-Type: application/json
{
"username":"admin",
"password":"admin",
}
response
fail
{
"status": 1,
"msg": "密碼錯誤"
}
success
{
"status": 0,
"data": {
"id": 12,
"username": "aaa",
"email": "aaa@163.com",
"phone": null,
"role": 0,
"createTime": 1479048325000,
"updateTime": 1479048325000
}
}
2.注冊
POST /user/register
request
{
"username":"admin",
"password":"admin",
"email":"admin@qq.com"
}
response
success
{
"status": 0,
"msg": "成功"
}
fail
{
"status": 2,
"msg": "用戶已存在"
}
3.獲取登錄用戶信息
GET /user
request
無參數
response
success
{
"status": 0,
"data": {
"id": 12,
"username": "aaa",
"email": "aaa@163.com",
"phone": null,
"role": 0,
"createTime": 1479048325000,
"updateTime": 1479048325000
}
}
fail
{
"status": 10,
"msg": "用戶未登錄,無法獲取當前用戶信息"
}
4.退出登錄
**POST /user/logout
request
無
response
success
{
"status": 0,
"msg": "退出成功"
}
fail
{
"status": -1,
"msg": "服務端異常"
}
分析接口:
由於項目是前后端分離的項目,故所有controller返回值都是json格式,可在controller層添加@ResponseBody實現。
由於前段發送http數據采用Content-Type: application/json 格式,故需要 在接受參數時使用@RequestBody 接受參數(如果前端采用:x-www-form-urlencoded ,則controller接口方法需要使用@RequestParam(value= "") 注解 接受參數)
分析所有用戶接口,返回值有 三個屬性 status,msg, data,為便於處理返回結果值,可新建返回值對象ResponseVo, 其中 data有時需要返回,有時不需要返回,可在ResponseVo對象添加@JsonInclude(value = JsonInclude.Include.NON_NULL)注解使其在序列化時排除屬性為null的值。同時考慮到data中的數據為User對象,后期也可能會是其他對象,故ResponseVo對象的data屬性需要用泛型
ResponseVo對象設計如下:
package cn.blogsx.mimall.vo;
import cn.blogsx.mimall.enums.ResponseEnum;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import org.springframework.validation.BindingResult;
import java.util.Objects;
/**
* @author Alex
* @create 2020-03-26 20:08
**/
@Data
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class ResponseVo<T> {
private Integer status;
private String msg;
private T data;
private ResponseVo(Integer status, String msg) {
this.status = status;
this.msg = msg;
}
private ResponseVo(Integer status, T data) {
this.status = status;
this.data=data;
}
}
根據需要添加構造方法ResponseVo(Integer status, String msg)、ResponseVo(Integer status, T data)
不同的錯誤需要不同的方法去返回相應,故可添加如下靜態方法:
public static <T> ResponseVo<T> successByMsg(String msg) {
return new ResponseVo<>(ResponseEnum.SUCCESS.getCode(),msg);
}
public static <T> ResponseVo<T> success(T data) {
return new ResponseVo<>(ResponseEnum.SUCCESS.getCode(),data);
}
public static <T> ResponseVo<T> success() {
return new ResponseVo<>(ResponseEnum.SUCCESS.getCode(),ResponseEnum.SUCCESS.getDesc());
}
public static <T> ResponseVo<T> error(ResponseEnum responseEnum) {
return new ResponseVo<>(responseEnum.getCode(),responseEnum.getDesc());
}
public static <T> ResponseVo<T> error(ResponseEnum responseEnum,String msg) {
return new ResponseVo<>(responseEnum.getCode(),msg);
}
public static <T> ResponseVo<T> error(ResponseEnum responseEnum, BindingResult bindingResult) {
return new ResponseVo<>(responseEnum.getCode(),
Objects.requireNonNull(bindingResult.getFieldError()).getField()+" "+
bindingResult.getFieldError().getDefaultMessage());
}
分析屬性:
status屬性的值為 整數,故使用Integer類型,為避免硬編碼,status應使用 枚舉對值做約束。
msg屬性分幾種情況:0對應的 成功、2對應的 用戶已存在、10對應的 用戶未登錄、-1對應的服務端錯誤(泛類型錯誤)
status和msg存在對應關系,可用枚舉類ResponseEnum做對應
package cn.blogsx.mimall.enums;
import lombok.Getter;
@Getter
public enum ResponseEnum {
ERROR(-1,"服務端錯誤"),
SUCCESS(0,"成功"),
PASSWORD_ERROR(1,"密碼錯誤"),
USERNNAME_EXIST(2,"用戶名已存在"),
PARAM_ERROR(3,"參數錯誤"),
EMAIL_EXIST(4,"郵箱已存在"),
NEED_LOGIN(10,"用戶未登錄,請先登錄"),
USERNAME_OR_PASSWORD_ERROR(11,"用戶名或密碼錯誤"),
;
Integer code;
String desc;
ResponseEnum(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
}
功能分析:
由於用戶登錄 只需要傳遞username和password而不需要傳遞其他參數,並且需要做參數校驗,所以為最好還是專門新建一個類專門用作傳參。注冊 同理
UserLoginForm類傳傳參封裝使用:
package cn.blogsx.mimall.form;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* @author Alex
* @create 2020-03-26 20:50
**/
@Data
public class UserLoginForm {
@NotBlank
private String username;
@NotBlank
private String password;
}
UserRegisterForm類傳傳參封裝使用:
package cn.blogsx.mimall.form;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
public class UserRegisterForm {
@NotBlank
private String username;
@NotBlank
private String password;
@NotBlank
private String email;
}
在注冊時需要將UserRegisterForm對象的屬性拷貝成user對象才能插入數據庫中,可使用Spring中提供的BeanUtils.copyProperties(userRegisterForm, user);方法實現。
關於javax.validation.constraints相關注解說明:
@NotBlank //用於String 判斷空格
@NotEmpty //用於集合
@NotNull //判斷null
使用如上注解校驗參數時只需在Controller層的接口方法上添加@Valid 以及 BindingResult bindingResult 形式參數,並在方法體中使用如下判斷並返回前端參數異常信息:
if (bindingResult.hasErrors()) {
return ResponseVo.error(PARAM_ERROR, bindingResult);
}
以上注解還可以定義參數異常原因:只需在使用注解時添加(message = "用戶名不能為空") 即可使用如下方法得到並返回給前端:
log.error("注冊提交參數有誤,{} {}",
Objects.requireNonNull(bindingResult.getFieldError()).getField(),
bindingResult.getFieldError().getDefaultMessage());
Objects.requireNonNull(bindingResult.getFieldError()).getField()是得到有誤從參數字段,bindingResult.getFieldError().getDefaultMessage()是得到我們在@NotBlank(message = "用戶名不能為空")寫入的message值,如果不寫,則默認為 “不能為空” 四個字。
具體實現接口的Controller方法:
package cn.blogsx.mimall.controller;
import cn.blogsx.mimall.consts.MiMallConst;
import cn.blogsx.mimall.form.UserLoginForm;
import cn.blogsx.mimall.form.UserRegisterForm;
import cn.blogsx.mimall.pojo.User;
import cn.blogsx.mimall.service.IUserService;
import cn.blogsx.mimall.vo.ResponseVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import java.util.Objects;
import static cn.blogsx.mimall.enums.ResponseEnum.PARAM_ERROR;
/**
* @author Alex
* @create 2020-03-26 19:43
**/
@RestController
@Slf4j
public class UserController {
@Autowired
private IUserService userService;
@PostMapping("/user/register")
public ResponseVo<User> register(@Valid @RequestBody UserRegisterForm userRegisterForm,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
log.error("注冊提交參數有誤,{} {}",
Objects.requireNonNull(bindingResult.getFieldError()).getField(),
bindingResult.getFieldError().getDefaultMessage());
return ResponseVo.error(PARAM_ERROR, bindingResult);
}
User user = new User();
BeanUtils.copyProperties(userRegisterForm, user);//使用Spring中拷貝對象之間的方法
return userService.register(user);
}
@PostMapping("/user/login")
public ResponseVo<User> login(@Valid @RequestBody UserLoginForm userLoginForm
, BindingResult bindingResult, HttpSession session) {
if (bindingResult.hasErrors()) {
return ResponseVo.error(PARAM_ERROR, bindingResult);
}
ResponseVo<User> userResponseVo = userService.login(userLoginForm.getUsername(), userLoginForm.getPassword());
//設置session
session.setAttribute(MiMallConst.CURRENT_USER, userResponseVo.getData());
log.info("login sessionId={}",session.getId());
return userResponseVo;
}
//session保存在內存里改進版:token+redis
@GetMapping("/user")
public ResponseVo<User> userInfo(HttpSession httpSession) {
User user = (User) httpSession.getAttribute(MiMallConst.CURRENT_USER);
return ResponseVo.success(user);
}
@PostMapping("/user/logout")
public ResponseVo logout(HttpSession httpSession) {
log.info("/user sessionId={}",httpSession.getId());
httpSession.removeAttribute(MiMallConst.CURRENT_USER);
return ResponseVo.success();
}
}
登錄有保存信息到session中,為降低耦合性,也可在專門新建常量MiMallConst類存儲session鍵常量:
package cn.blogsx.mimall.consts;
/**
* @author Alex
* @create 2020-03-26 22:33
**/
public class MiMallConst {
public static final String CURRENT_USER = "currentUser";
}
session默認有效時間為30分鍾。也可在application文件中做如下配置:
server:
servlet:
session:
timeout: 120 #以秒為單位 兩分鍾
為方便統一管理判斷用戶是否登錄,可使用攔截器做統一攔截:
package cn.blogsx.mimall;
import cn.blogsx.mimall.consts.MiMallConst;
import cn.blogsx.mimall.exception.UserloginException;
import cn.blogsx.mimall.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @Dscription: 登錄攔截器
* @author Alex
* @create 2020-03-27 10:13
**/
@Slf4j
public class UserLoginInterceptor implements HandlerInterceptor {
/**
* true 表示繼續流程,false 表示中斷
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("preHandle...");
User user = (User) request.getSession().getAttribute(MiMallConst.CURRENT_USER);
if (user == null) {
log.info("user=null");
throw new UserloginException();
// return ResponseVo.error(ResponseEnum.NEED_LOGIN);
// return false;
}
return true;
}
}
此時攔截器還未生效,需要在配置攔截路徑,新建InterceptorConfig配置類:
package cn.blogsx.mimall;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author Alex
* @create 2020-03-27 10:21
**/
@Configuration //使用該注解才能在Spring啟動后起作用
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserLoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/user/login","/user/register");//登錄注冊接口無需攔截
}
}
Service接口及方法:
package cn.blogsx.mimall.service;
import cn.blogsx.mimall.pojo.User;
import cn.blogsx.mimall.vo.ResponseVo;
public interface IUserService {
/**
* 注冊
*/
ResponseVo<User> register(User user);
/**
* 登錄
*/
ResponseVo<User> login(String username,String password);
}
實現方法:
package cn.blogsx.mimall.service.impl;
import cn.blogsx.mimall.dao.UserMapper;
import cn.blogsx.mimall.enums.RoleEnum;
import cn.blogsx.mimall.pojo.User;
import cn.blogsx.mimall.service.IUserService;
import cn.blogsx.mimall.vo.ResponseVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import java.nio.charset.StandardCharsets;
import static cn.blogsx.mimall.enums.ResponseEnum.*;
/**
* @author Alex
* @create 2020-03-26 17:34
**/
@Service
public class UserServiceImpl implements IUserService {
@Autowired
private UserMapper userMapper;
@Override
public ResponseVo<User> register(User user) {
//username不能重復
int countByUsername = userMapper.countByUsername(user.getUsername());
if(countByUsername>0) {
return ResponseVo.error(USERNNAME_EXIST);
}
//email不能重復
int countByEmail = userMapper.countByEmail(user.getEmail());
if(countByEmail>0) {
return ResponseVo.error(EMAIL_EXIST);
}
user.setRole(RoleEnum.CUSTOMER.getCode());
//MD5摘要算法(Spring自帶)
user.setPassword( DigestUtils.md5DigestAsHex(user.getPassword()
.getBytes(StandardCharsets.UTF_8)));
//寫入數據庫
int resultCount = userMapper.insertSelective(user);
if(resultCount==0) {
return ResponseVo.error(ERROR);
}
return ResponseVo.success();
}
@Override
public ResponseVo<User> login(String username, String password) {
User user = userMapper.selectByUsername(username);//登錄查詢推薦只使用username查詢即可
if(user == null) {
//用戶不存在,返回 用戶名或密碼錯誤
return ResponseVo.error(USERNAME_OR_PASSWORD_ERROR);
}
if(!user.getPassword().equalsIgnoreCase(
DigestUtils.md5DigestAsHex(
password.getBytes(StandardCharsets.UTF_8)))) {
//密碼錯誤,返回 用戶名或密碼錯誤
return ResponseVo.error(USERNAME_OR_PASSWORD_ERROR);
}
user.setPassword("");
return ResponseVo.success(user);
}
}
使用Srping框架無需再單獨寫Utils方法做MD5加密,Spring中自帶MD5加密方法: DigestUtils.md5DigestAsHex
如果服務端發生其他異常,例如為500的異常也需要返回json格式的提示,此時可以新建RuntimeExcetion異常處理類:
package cn.blogsx.mimall.exception;
import cn.blogsx.mimall.enums.ResponseEnum;
import cn.blogsx.mimall.vo.ResponseVo;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import static cn.blogsx.mimall.enums.ResponseEnum.ERROR;
/**
* @author Alex
* @create 2020-03-26 21:43
**/
@ControllerAdvice
public class RuntimeExceptionHandler {
@ExceptionHandler(RuntimeException.class)
@ResponseBody
// @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)//可修改http協議狀態碼
public ResponseVo handler(RuntimeException e) {
return ResponseVo.error(ERROR,e.getMessage());
}
@ExceptionHandler(UserloginException.class)
@ResponseBody
public ResponseVo userLoginHandler() {
return ResponseVo.error(ResponseEnum.NEED_LOGIN);
}
}
由於用戶賬號和密碼的錯誤采用HandlerInterceptor的實現方法攔截,且其preHandle方法返回值只能是true或false以用作判斷是否繼續流程(true 表示繼續流程,false 表示中斷)所以為了返回給前端 可新建UserloginException異常做異常處理
public class UserloginException extends RuntimeException {
}
只需繼承RuntimeException即可,只要有該類名即可,真正處理異常的地方在 RuntimeExceptionHandler中。
關於測試:一般來說 開發人員單測只需測試service即可,controller層為測試人員測試。
idea中可在serviceimpl 類中 右鍵空白處-> Go To ->test 即可在mavne test目錄下自動生成測試類。
Maven 打包是執行單測:
mvn clean package
Mavnen打包時跳過單測:
mvn clean package -Dmaven.test.skip=true