如要开发如下用户接口:
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
}
}
POST /user/register
request
{
"username":"admin",
"password":"admin",
"email":"admin@qq.com"
}
response
success
{
"status": 0,
"msg": "成功"
}
fail
{
"status": 2,
"msg": "用户已存在"
}
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": "用户未登录,无法获取当前用户信息"
}
**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
原文:https://www.cnblogs.com/sxblog/p/12580741.html