秒杀项目-学习笔记3
实现登录2.2
jsr303参数校验
- 引入依赖
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
- 在controller层中,给需验证的传入参数加上@Valiad,找到需验证的参数的对象,给此对象中需要进行验证的参数添加相应的注解。如,给LoginController类中的doLogin方法的接收参数loginVo前添加@Valiad注解,因为loginVo是LoginVo类的对象,所以进入到LoginVo类中,对意味电话号码的mobile和密码password进行注解添加,首先两者均不可为空,因此给两个属性前都添加@NotNull,表示禁止此属性值为null,再使用 @Length(min=限制的长度)规定密码的长度,为保证输入手机号码是合法的再添加自定义注解@IsMobie。
- 为实现自定义注解,先再启动类的同包下创建一个validator包,用于存放校验模型,在包中创建IsMobie类,将此类的改为注解类,进入到@NotNoll注释对应的类中,将其中对类进行的注解都复制在IsMobile类前,再将@NotNoll注释对应的类中的message、groups和payload属性都复制到IMobile接口类中。将如果校验不通过的信息写在message的默认值中。
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = {IsMobileValidator.class}
)
public @interface IsMobile {
boolean required() default true;//运行在特殊时刻为空。如不传参数
String message() default "手机号码格式错误"; //校验不通过时提示信息
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
补充:
[ 1 ].default的多种应用方法
(1)default在方法和类前是表示的是同一包中的类可以访问,声明时没有加修饰符,认为是friendly。
(2)default 是Java8中引入的关键字,它可以使一个具体的类存在与一个接口类中,在接口类的实现类中可以不用重写这个具体方法。
public interface testDefault {
void test2();
default void test1(){
System.out.println("this is a specific class");
}
}
class testClass implements testDefault{
@Override
public void test2() {
System.out.println("this is a abstract class by rewrite");
}
}
(3)default在switch-case中,就是当case里的值与switch里的key没有匹配的时候,执行default里的方法。
(4)在注解属性中default后的值为此对象的默认值。
[ 2 ].自定义注解类所用到的各种注解的含义
(1)@Target:对注解的作用目标的配置注解,其中的属性ElementType按照注释可能出现在Java中的语法位置,提供了一个简单的分类,各取值的意思分别是,
······TYPE:类,接口或是枚举声明之前
······FIELD:字段声明之前
······METHOD:方法声明之前
······PARAMETER:正式的参数声明之前
······CONSTRUCTOR:构造函数声明之前
······LOCAL_VARIABLE:局部变量声明之前
······ANNOTATION_TYPE:注释类型声明之前
······PACKAGE:包声明之前
(2)@Retention:用来说明该注解类的生命周期,有三个参数分别为,
······RetentionPolicy.SOURCE : 注解只保留在源文件中;
······RetentionPolicy.CLASS : 注解保留在class文件中,在加载到JVM虚拟机时丢弃;
······RetentionPolicy.RUNTIME : 注解保留在程序运行期间,此时可以通过反射获得定义在某个类上的所有注解。
(3)@Documented:用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化
(4)@Constraint:用于处理注解的逻辑,也就是来限定自定义注解的方法,将校验器进行引入
- 为创建用于实现判断手机号是否合规的逻辑,创建IsMobileValidator类,是这个类实现ConstraintValidator<自定义注解名称,注解修饰的类型>接口。将实现的接口类进行重写,initialize方法用于初始化,isValid方法用于实现判断是否合法的逻辑。新建用于判断是否为空的boolean属性required,将其默认值设为false。
在isValid中根据required的值判断信息是否为必须的,如果不为空就是必须的,调用于之前创建的判断手机号格式的ValidatorUtil类的方法,来进行对手机号码的合法性的判断。
public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {
private boolean required = false;
@Override
public void initialize(IsMobile isMobile) {
required = isMobile.required();
}
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if (required){
return ValidatorUtil.isMobile(s);
}else {
if (StringUtils.isEmpty(s)){
return true;
}else {
return ValidatorUtil.isMobile(s);
}
}
}
}
补充:
这个IsMobileValidator因为实现了ConstraintValidator,所以它默认被spring管理成bean,可以在这个逻辑处理类里面用@Autowiredu或者@Resources注入别的服务,而且不用在类上面用@Compent注解成spring的bean。
- 这样参数校验功能就实现了,可以将controller中参数校验部分的代码进行删除。
异常处理
重新运行程序,输入错误的手机号码,但是并没有出现,手机号码格式错误的提示信息显示出来。打开浏览器的开发者工具可以看到,校验器是正常工作的,为使页面的显示更为友好,加入异常拦截。
自定义异常拦截器
- 在起始类同包下创建exception包,并在这个包下创建GlobleExceptionHandler类。为这个类添加@ControllerAdvice注解,实现全局异常处理。为方便输出将java对象转为json格式的数据,添加@ResponseBody注解。编写在拦截到异常时需做的事情的逻辑实现方法,并给它添加@ExceptionHandler,在这个注解的value中规定拦截的异常的类型。
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class)
public Result<String> exceptionHandler(HttpServletRequest request, Exception e){
e.printStackTrace();
if (e instanceof GlobalException){
GlobalException ex = (GlobalException)e;
return Result.error(ex.getCodeMsg());
}else if (e instanceof BindException){//判断是否为绑定异常
BindException ex = (BindException)e; //强转
List<ObjectError> errors = ex.getAllErrors(); //获取所有的错误
ObjectError error = errors.get(0); //拿到第一个错误
String msg = error.getDefaultMessage(); //拿到错误信息
return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg)); //拼接完之后的message应该是 参数校验异常:msg的内容
}else {
return Result.error(CodeMsg.SERVER_ERROR);
}
}
}
- 优化:
(1)定义一个全局异常类。继承RuntimeException,创建CodeMsg类的cm属性,并创建为其赋值的该类的构造方法。给类添加@Data,用于避免手动生成set和get方法。
@Data
public class GlobalException extends RuntimeException {
private CodeMsg codeMsg;
public GlobalException(CodeMsg codeMsg) {
super(codeMsg.getMsg());
this.codeMsg = codeMsg;
}
}
(2)修改service类中的代码,将login方法中返回值类型换为boolean类型,若原代码中返回的为异常情况,就将其替换为使用throw的异常抛出,若全程没有错误,就返回true。
public boolean login(LoginVo loginVo) {
if(loginVo == null){
throw new GlobalException(CodeMsg.SERVER_ERROR); //出现异常直接往外抛
}
String mobile = loginVo.getMobile();
String formPass = loginVo.getPassword();
//判断手机号是否存在
MiaoshaUser user = getById(Long.parseLong(mobile));
if(user == null){
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
}
//验证密码
String dbPass = user.getPassword();
String saltDB = user.getSalt();
String calcPass = MD5Util.formPassToDBPass(formPass, saltDB);
if (!calcPass.equals(dbPass)){
throw new GlobalException(CodeMsg.PASSWORD_ERROR);
}
return true;
}
(3)修改Controller层的代码,将doLogi中的方法改为直接调用service的login方法,因为如果有异常会直接抛出,所以返回时直接返回成功。
@RequestMapping("/do_login")
@ResponseBody
public Result<Boolean> doLogin(@Valid LoginVo loginVo){
log.info(loginVo.toString());
//登录
miaoshaUserService.login(loginVo);
return Result.success(true);
}
分布式Session(重要)
开发时如果是多台服务器,session有可能落不到同一台服务器上从而造成session数据的丢失。虽然可以用session同步来解决这个问题,但是在实际应用中却很少这么做,因为如果同步的服务器过多是一件很恐怖的事情。
原理:通过uuid获取session
实现:
- 生成cookie
(1)在MiaoshaUserService中,找到login方法。在通过手机号码、密码和服务的校验后,开始写生成cookie步骤。
(2) 在util包中创建UUIDUtil类。
public class UUIDUtil {
public static String uuid(){
return UUID.randomUUID().toString().replace("-", ""); //去掉原生自带的"-"
}
}
(3)想通过UUID生产token,要将这个token写在cookie中,传递给客户端,为区别不同的用户,需对用户进行表示。将用户写在redis当中,将RedisService对象引入这个类。
(4)为了给token生成一个prefix,在redis类中为用户创建一个新的key类,在其中创建一个token,其值为”tk“。设置过期时间TOKEN_EXPIRE。
public class MiaoshaUserKey extends BasePrefix {
public static final int TOKEN_EXPIRE = 3600*24*2;
public MiaoshaUserKey(int expireSeconds, String prefix) {
super(expireSeconds, prefix);
}
public static MiaoshaUserKey token = new MiaoshaUserKey(TOKEN_EXPIRE, "tk");
}
(5)返回到MiaoshaUserService的login方法中,通过RedisService的set方法向redis中存储数据。
(6) 新建Cookie,新建对象需要name和value两个值,新建Cookie的name。将cookie的name和之前的token分别作为,cookie的name和value添加在Cookie的new对象中。
(7)为Cookie设置时效,如果miaoshaKey中的token过期了,那么这个cookie就过期了。
(8) 设置Cookie的路径为根目录,并给login方法传入的数据中添加HttpServletResponse对象。用response的addCookie方法,将Cookie写入客户端。
//生成cookie
String token = UUIDUtil.uuid();
redisService.set(MiaoshaUserKey.token, token, user); //将user和token绑定并存入Redis中
Cookie cookie = new Cookie(COOKIE_NAME_TOKEN, token); //根据token生成cookie
cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
cookie.setPath("/");
response.addCookie(cookie); //将cookie放入response客户端中
(9)到调用了MiaoshaUserService类中的login方法的controller类中,给从前端传入的值中添加HttpServletResponse对象,并将其加入login方法的调用中。
- 登录后跳转页面
(1)在resources包下的templates,创建goods_list.html页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>商品列表</title>
</head>
<body>
<p th:text="'hello:' + ${user.nickname}"></p>
</body>
</html>
(2)新建Controller,用于跳转页面。添加类中添加list方法,返回值为goods_list页面的名称。
@Controller
@RequestMapping("/goods")
public class GoodsController {
@Autowired
MiaoshaUserService miaoshaUserService;
@RequestMapping("/to_list")
public String list(Model model) {
model.addAttribute("user", new MiaoshaUser());
return "goods_list";
}
}
(3)在login.html中,在登录成功后添加跳转goods_list页面的跳转语句。
window.location.href="/goods/to_list";
(4)运行测试:跳转成功,并且cookie生成成功
(5)因为cookie已经成功的上传到了客户端中,所以可以直接获取客户端中的cookie,对GoodsController类进行修改。用@CookieValue来获取指定value的cookie值,value为token的name。大部的手机为了兼容性,都不将token存在cookie中,所以再使用@RequestPparam获取cookie。为两种获取cookie的方法设定优先级,先取RequestPparam中的cookie如果取不到,再取CookieValue中的。
@Controller
@RequestMapping("/goods")
public class GoodsController {
private static Logger log = LoggerFactory.getLogger(LoginController.class);
@Autowired
MiaoshaUserService miaoshaUserService;
@Autowired
RedisService redisService;
@RequestMapping("/to_list")
public String list(Model model, @CookieValue(value=MiaoshaUserService.COOKIE_NAME_TOKEN,required = false)String cookieToken,
@RequestParam(value=MiaoshaUserService.COOKIE_NAME_TOKEN,required = false)String paramToken) {
if(StringUtils.isEmpty(cookieToken)&&StringUtils.isEmpty(paramToken)){
return "login";
}
String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;//设定优先级
log.info("this is goodcontroller"+token);
MiaoshaUser user = miaoshaUserService.getByToken(token);
model.addAttribute("user",user);
return "goods_list";
}
}
注意:如果运行出现500错误,并提示“Required String parameter ‘token’ is not present”
可能是给cookieToken和paramToken漏添加了required = false,将俩个数值都会设为不是必须的。
(5)找到MiaoshaService,创建getByToken方法,在其中创建一个方法用于从缓存中取值。这个类中必不可少的是一定要有参数认证。
public MiaoshaUser getByToken(String token) {
if (StringUtils.isEmpty(token)){
return null;
}
return redisService.get(MiaoshaUserKey.token, token, MiaoshaUser.class);
}
延长token的时效:
(1)重新把缓存重点值获取一下,创建新的cookie,将数据写进去。先将生成cookie的代码分割出来。将其复制在新建的addCookie中。
(2)在原生成cookie的地方,调用addCookie方法,并将这一句复制在getByToken中,这样就实现了新建cookie。这样修改后,在功能上实现了,但是并不优雅。每次调用方法时传入的值都过多。
将代码优雅:
(1)为使方法传入参数比较少,写一个类,将传入参数都获取一遍。创建新的继承WebMvcConfigurer类的Adapter类WebConfig子类,并为其添加@Configuration注解。
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired
UserArgumentResolver userArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(userArgumentResolver);
}
}
(2)创建逻辑实现类UserArgumentResolver,实现了HandlerMethodArgumentResolver接口。将controller中对获取到的token参数的操作都移植到resolveArgument方法下。通过捕捉所有的cookie,进行遍历来获取我们所需要的cookie。
@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
MiaoshaUserService miaoshaUserService;
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
Class<?> clazz = methodParameter.getParameterType(); //获取参数的类型
return clazz == MiaoshaUser.class; //如果为真,才会执行下面的resolveArguement方法
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class); //先拿到request和response
HttpServletResponse response = nativeWebRequest.getNativeResponse(HttpServletResponse.class);
String paramToken = request.getParameter(MiaoshaUserService.COOKIE_NAME_TOKEN); //再拿到paramToken和cookieToken
String cookieToken = getCookieValue(request, MiaoshaUserService.COOKIE_NAME_TOKEN);
if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)){
return null;
}
String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken; //paramToken优先使用
return miaoshaUserService.getByToken(response, token);
}
private String getCookieValue(HttpServletRequest request, String cookieNameToken) {
Cookie[] cookies = request.getCookies(); //获取所有的cookie
for (Cookie cookie : cookies){
if (cookie.getName().equals(cookieNameToken)){
return cookie.getValue();
}
}
return null;
}
}