项目搭建
1. SpringBoot环境搭建
1.1 pom.xml-com.example.miao
主要依赖 dependency
spring-boot-starter-thymeleaf --模板引擎
spring-boot-starter-web --springMVC
mysql-connector-java --mysql
lombok
spring-boot-starter-tomcat
spring-boot-starter-test
mybatis-plus-boot-starter --mybatis-plus
commons-lang3
commons-codec
spring-boot-starter-validation --validation
spring-boot-starter-data-redis --redis
commons-pool2
spring-session-data-redis --springsession
spring-boot-starter-amqp --amqp
fastjson
easy-captcha 验证码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>miaosha</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>miaosha</name>
<description>miaosha</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.6</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.5.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.5.6</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<!--<version>2.5.3</version> -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
<!--
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**
1.2 application,yml
如下配置
spring
resources
thymleaf
redis
datasource
hikari
rabbitmq
mybatis-plus
logging
spring:
resources:
cache:
cachecontrol:
max-age: 3600
chain:
cache: true
enabled: true
compressed: true
html-application-cache: true
add-mappings: true
static-locations: classpath:/static/
thymeleaf:
cache: false
redis:
host: 192.168.25.130
port: 6379
database: 0
timeout: 10000ms
lettuce:
pool:
max-active: 8
max-wait: 10000ms
max-idle: 200
min-idle: 5
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/miaosha?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
hikari:
username: root
password: 123456
hikari:
pool-name: DateHikariCP
minimum-idle: 5
idle-timeout: 1800000
maximum-pool-size: 10
auto-commit: true
max-lifetime: 1800000
connection-timeout: 30000
connection-init-sql: SELECT 1
rabbitmq:
username: guest
host: 192.168.25.130
virtual-host: /
port: 5672
listener:
simple:
concurrency: 10
max-concurrency: 10
prefetch: 1
auto-startup: true
default-requeue-rejected: true
template:
retry:
enabled: true
initial-interval: 1000ms
max-attempts: 3
max-interval: 10000ms
multiplier: 1
mybatis-plus:
mapper-locations: classpath*:/mapper/*Mapper.xml
type-aliases-package: com.example.miaosha.pojo
logging:
level:
com.example.miaosha.mapper: debug
1.3 MiaoshaApplication.java
package com.example.miaosha;
import org.junit.jupiter.api.Test;
import org.junit.platform.commons.util.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.RedisScript;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@SpringBoot
Application
@MapperScan("com.example.miaosha.mapper")
public class MiaoshaApplication{
public static void main(String[] args) {
SpringApplication.run(MiaoshaApplication.class, args);
}
}
1.3 WebConfig.java com.example.miaosha.config
contoller 中 如果需要引入User
addArgumentResolvers(UserArgumentResolve)
接口方案添加限流注解(AccessLimit),需要添加拦截器
addInterceptors(AccessLimitInterceptor)
package com.example.miaosha.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Autowired
private UserArgumentResolve userArgumentResolve;
@Autowired
private AccessLimitInterceptor accessLimitInterceptor;
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/META-INF/resources/", "classpath:/resources/",
"classpath:/static/", "classpath:/public/" };
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(userArgumentResolve);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!registry.hasMappingForPattern("/webjars/**")) {
registry.addResourceHandler("/webjars/**").addResourceLocations(
"classpath:/META-INF/resources/webjars/");
}
if (!registry.hasMappingForPattern("/**")) {
registry.addResourceHandler("/**").addResourceLocations(
CLASSPATH_RESOURCE_LOCATIONS);
}
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor( accessLimitInterceptor);
}
}
1.4 UserArgumentResolve.java com.example.miaosha.config
原先通过request获取cookie中的userTicket,再去redis获取缓存的User对象
后面在接口限流注解(AccessLimit)时,已经把User对象放在 ThreadLocal中,所以改为从 UserContext中直接获取
package com.example.miaosha.config;
import com.example.miaosha.pojo.User;
import com.example.miaosha.service.IUserService;
import com.example.miaosha.utils.CookieUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class UserArgumentResolve implements HandlerMethodArgumentResolver {
@Autowired
private IUserService userService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> clazz = parameter.getParameterType();
return clazz== User.class;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
return UserContext.getUser();
}
}
1.5 AccessLimit.java com.example.miaosha.config
method 限流注解
@AccessLimit(second=5,maxCount=5,needLogin=true)
package com.example.miaosha.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {
int second();
int maxCount();
boolean needLogin() default true;
}
1.6 UserContext com.example.miaosha.config
把User对象放在线程本地对象中,在AccessLimitInterceptor中 set
package com.example.miaosha.config;
import com.example.miaosha.pojo.User;
public class UserContext {
private static ThreadLocal<User> userHolder=new ThreadLocal<User>();
public static void setUser(User user){
userHolder.set(user);
}
public static User getUser(){
return userHolder.get();
}
}
1.7限流 拦截器AccessLimitInterceptor com.example.miaosha.config
判断method有没有添加AccessLimit注解,如果有注解,直接先去判断 是否 短时间之内请求次数太多
package com.example.miaosha.config;
import com.alibaba.fastjson.JSON;
import com.example.miaosha.pojo.User;
import com.example.miaosha.service.IUserService;
import com.example.miaosha.utils.CookieUtil;
import com.example.miaosha.vo.RespBean;
import com.example.miaosha.vo.RespBeanEnum;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.concurrent.TimeUnit;
@Component
@Slf4j
public class AccessLimitInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private IUserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(handler instanceof HandlerMethod){
User user=getUser(request,response);
UserContext.setUser(user);
HandlerMethod hm = (HandlerMethod) handler;
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
if(accessLimit==null){
return true;
}
int second = accessLimit.second();
int maxCount = accessLimit.maxCount();
boolean needLogin = accessLimit.needLogin();
String key=request.getRequestURI();
if(needLogin){
if(user==null){
log.info("AccessLimitInterceptor:"+"需要登录");
render(response,RespBeanEnum.SESSION_ERROR);
return false;
}
key= ":" + user.getId();
}
ValueOperations valueOperations = redisTemplate.opsForValue();
Integer count = (Integer)valueOperations.get(key);
if(null==count){
valueOperations.set(key ,1,5, TimeUnit.SECONDS);
}else if(count>5){
log.info("AccessLimitInterceptor:"+"短时间之内请求次数太多");
render(response,RespBeanEnum.REQUEST_LIMITED);
return false;
}else {
valueOperations.increment(key);
}
return true;
}
return true;
}
private void render(HttpServletResponse response, RespBeanEnum respBeanEnum) throws IOException {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
PrintWriter out=response.getWriter();
RespBean error = RespBean.error(respBeanEnum);
out.write(new ObjectMapper().writeValueAsString(error));
out.flush();
out.close();
}
private User getUser(HttpServletRequest request,HttpServletResponse response) {
String ticket = CookieUtil.getCookieValue(request, "userTicket");
if(StringUtils.isEmpty(ticket)){
return null;
}
return userService.getUserByCookie(ticket, request, response);
}
}
2. 集成Thymleaf,RespBean
参照主yml的配置
2.1 RespBean.java com.example.miaosha.vo
package com.example.miaosha.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RespBean implements Serializable {
private static final long serialVersionUID = 1L;
private long code;
private String message;
private Object obj;
public static RespBean success(){
return new RespBean(RespBeanEnum.SUCCESS.getCode(),RespBeanEnum.SUCCESS.getMessage(),null);
}
public static RespBean success(Object obj){
return new RespBean(RespBeanEnum.SUCCESS.getCode(),RespBeanEnum.SUCCESS.getMessage(),obj);
}
public static RespBean error(RespBeanEnum respBeanEnum){
return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),null);
}
public static RespBean error(RespBeanEnum respBeanEnum,Object obj){
return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),obj);
}
}
2.2 RespBeanEnum.java com.example.miaosha.vo
package com.example.miaosha.vo;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
@Getter
@ToString
@AllArgsConstructor
public enum RespBeanEnum {
SUCCESS(200,"SUCCESS"),
ERROR(500,"服务异常"),
LOGIN_ERROR(500210,"用户名或密码不正确"),
MOBILE_ERROR(500211,"手机号格式不正确"),
SESSION_ERROR(500212,"用户不存在"),
REQUIRE_LOGIN_ERROR(500221,"需要重新登录"),
UPDATE_PASSWORD_ERROR(500222,"密码更新不成功"),
BIND_ERROR(500300,"参数校验异常"),
EMPTY_STOCK(500501,"商品库存为空"),
REPEATE_ERROR(500502,"重复秒杀"),
SECKILL_ERROR(500504,"没有秒杀到"),
REQUEST_ILLEGAL(500505,"秒杀请求非法"),
CAPTCHA_ERROR(500506,"验证码不正确"),
REQUEST_LIMITED(500506,"短时间之内请求次数太多"),
ORDER_NOT_EXIST(500401,"订单不存在")
;
private final Integer code;
private final String message;
}
3.集成mybatis
参照主yml的配置