秒杀功能
参数校验
在任何时候,当你要处理一个应用程序的业务逻辑,数据校验是你必须要考虑和面对的事情。应用程序必须通过某种手段来确保输入进来的数据从语义上来讲是正确的。在通常的情况下,应用程序是分层的,不同的层由不同的开发人员来完成。很多时候同样的数据验证逻辑会出现在不同的层,这样就会导致代码冗余和一些管理的问题,比如说语义的一致性等。为了避免这样的情况发生,最好是将验证逻辑与相应的域模型进行绑定。
用 JSR 303 – Bean Validation 规范 :
Bean Validation 中的 constraint
表 1. Bean Validation 中内置的 constraint
Constraint | 详细信息 |
---|---|
@Null |
被注释的元素必须为 null |
@NotNull |
被注释的元素必须不为 null |
@AssertTrue |
被注释的元素必须为 true |
@AssertFalse |
被注释的元素必须为 false |
@Min(value) |
被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) |
被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) |
被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) |
被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max, min) |
被注释的元素的大小必须在指定的范围内 |
@Digits (integer, fraction) |
被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Past |
被注释的元素必须是一个过去的日期 |
@Future |
被注释的元素必须是一个将来的日期 |
@Pattern(value) |
被注释的元素必须符合指定的正则表达式 |
表 2. Hibernate Validator 附加的 constraint
Constraint | 详细信息 |
---|---|
@Email |
被注释的元素必须是电子邮箱地址 |
@Length |
被注释的字符串的大小必须在指定的范围内 |
@NotEmpty |
被注释的字符串的必须非空 |
@Range |
被注释的元素必须在合适的范围内 |
一个 constraint 通常由 annotation 和相应的 constraint validator 组成,它们是一对多的关系。也就是说可以有多个 constraint validator 对应一个 annotation。在运行时,Bean Validation 框架本身会根据被注释元素的类型来选择合适的 constraint validator 对数据进行验证。
有些时候,在用户的应用中需要一些更复杂的 constraint。Bean Validation 提供扩展 constraint 的机制。可以通过两种方法去实现,一种是组合现有的 constraint 来生成一个更复杂的 constraint,另外一种是开发一个全新的 constraint。
Controller 中需要校验的参数Bean前添加 @Valid 开启校验功能
添加依赖:JSP303的依赖应该放在api中
<!-- 参数校验JSR303 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
使用:
Controller 中需要校验的参数Bean前添加 @Valid 开启校验功能
例子:
public class User {
private Integer id;
"用户名不能为空") (message =
private String username;
"^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,16}$", message = "密码必须为8~16个字母和数字组合") (regexp =
private String password;
private String email;
private Integer gender;
}
方式1:controller中可以通过将错误的信息封装进result中返回给前台,让前台展示:
"/user") (
public class UserController {
"") (
public Result save ( User user , BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
Map<String , String> map = new HashMap<>();
bindingResult.getFieldErrors().forEach( (item) -> {
String message = item.getDefaultMessage();
String field = item.getField();
map.put( field , message );
} );
return Result.build( 400 , "非法参数 !" , map);
}
return Result.ok();
}
}
方式2:参数校验不通过时,会抛出 BingBindException 异常,可以在统一异常处理中,做统一处理,这样就不用在每个需要参数校验的地方都用 BindingResult 获取校验结果了。
测试:
秒杀功能
将表个导入数据库中;
项目的结构搭建:注意组件之间的依赖抽取和继承的关系。一定要懂。
给server项目添加配置文件和启动类;
在远程配置仓库配置对应的服务配置文件,连接数据库的四要素,mybatis等配置。都是复制修改的操作,流程要熟。
修改zull-server网关的配置文件中的内容:添加两个server微服务的路由配置:还是复制修改。
测试:启动网关和服务,看端口和服务是否正常。
商品列表页展示
创建good-api和good-server项目,添加bootstrap.yml和good-server.yml文件
在good-server的pom文件中引入mysql,druid,mybatis依赖
在good-api里面添加Good实体类
把mapper,service,feign,hystrix定义好.
在api中创建domain的类:可以拷贝
复制静态页面的前端代码;
根据前端的请求,按顺序完成controller,service,mapper的构建;
query返回的数据类型是:
秒杀商品列表中包含了秒杀列表和商品列表两个表中的字段,即两个表新组合的表,使用VO对象来封装他们组合的字段。返回类型和值是什么?——类型:Result<List<SeckillGoodVO>>
;值:return Result.success(seckillGoodService.query());
业务层的实现:
查询秒杀商品列表:
查询商品秒杀的列表(Mapper中注解的方式查SQL)
根据商品秒杀的列表,得到商品 id 的列表;
商品 id 的列表应该是 List 还是 Set:
通过 feign 远程调用商品服务,查询列表;
遍历商品列表,将列表中每项的属性,复制到 VO 中,得到 vo 列表
知识点:用 HashMap 做缓存;(空间换时间:用HashMap做缓存,不然两重遍历太费时);属性复制的顺序
@Override
public List<SeckillGoodVO> query() {
// 秒杀商品列表的查询
// 1.查询商品秒杀的列表
List<SeckillGood> seckillGoodList = seckillGoodMapper.listAll();
// 2.根据商品秒杀的列表,得到商品 id 的列表, ids 应该是Set集合,因为不同场秒杀关联的商品可能相同
Set<Long> goodIds = new HashSet<>(seckillGoodList.size());
for (SeckillGood seckillGood : seckillGoodList) {
goodIds.add(seckillGood.getId());
}
// 3.通过 feign 远程调用商品服务,查询列表
List<Good> goodList = null; // TODO 商品服务的远程调用还没有完成
// 通过遍历的方式来给属性赋值,效率低。采用HashMap做缓存的方式来操作
HashMap<Long, Good> tempCache = new HashMap<>(goodList.size());
// 把查到的列表先丢到map中
for (Good good : goodList) {
tempCache.put(good.getId(), good);
}
// 4.遍历商品列表,将列表中每项的属性,复制到 VO 中,得到 vo 列表
List<SeckillGoodVO> seckillGoodVOList = new ArrayList<>(seckillGoodList.size());
for (SeckillGood seckillGood : seckillGoodList) {
Good good = tempCache.get(seckillGood.getGoodId());// 拿到map中的值
SeckillGoodVO vo = new SeckillGoodVO(); // 创建vo对象,复制两者属性
// ------ 注意属性复制的顺序:vo的id应该和秒杀商品的id相同 -------
// * 复制商品的属性
if (good != null)
//有值才复制属性:BeanUtils
BeanUtils.copyProperties(good, vo);
// * 复制秒杀商品的属性
BeanUtils.copyProperties(seckillGood, vo);
seckillGoodVOList.add(vo); // 添加进list中
}
return seckillGoodVOList;
}
远程调用商品服务 —— 拿到商品列表
商品夫妇接口的返回类型应该是什么?—— Result<List<Good>>
因为该服务可能返回的数据有:正常返回数据;出现异常报错(code != 200);响应慢或异常执行降级返回null。 所以这里返回result对象(封装好了各种的情况,解决了异常传递的问题,否则报类型转换异常)
good-api的接口:
public interface GoodFeignApi {
Result<List<Good>> selectGoodListByIds(Long> ids); Set<
}
GoodFeignClient:
业务层实现方法:
Mapper中注解方式SQL的foreach(IN语句的时候):
public interface GoodServiceMapper {
// 复杂的 SQL 语句注解方式比较麻烦
List<Good> selectGoodListByIds(Long> ids); Set<
// 内部类实现查询 SQL 的拼接
class GoodServiceProvider {
// 返回的应该是 String , 它是 SQL 语句
public String selectGoodListByIds( Set<Long> ids) {
StringBuilder sql = new StringBuilder(100);
sql.append("SELECT * FROM t_goods");
if (CollectionUtils.isEmpty(ids))
return sql.toString(); // 没有 id SQL是: select * from t_goods
// 有内容 :SQL 后面跟上 WHERE id IN (1,2,3)
sql.append(" WHERE id IN ("); // 注意前面的空格
for (Long id : ids) {
sql.append(id).append(",");
}
// 去掉最后一个逗号
sql.deleteCharAt(sql.length() - 1);
// 最后右半边括号
sql.append(")");
return sql.toString();
}
}
}
调用商品服务完善秒杀商品的业务实现:
业务完整代码:
public class SeckillGoodServiceImpl implements ISeckillGoodService {
private SeckillGoodMapper seckillGoodMapper;
private GoodFeignApi goodFeignApi; // 涉及RPC 检查启动类有无注解:@EnableFeignClients
public List<SeckillGoodVO> query() {
// 秒杀商品列表的查询
// 1.查询商品秒杀的列表
List<SeckillGood> seckillGoodList = seckillGoodMapper.listAll();
// 2.根据商品秒杀的列表,得到商品 id 的列表, ids 应该是Set集合,因为不同场秒杀关联的商品可能相同
Set<Long> goodIds = new HashSet<>(seckillGoodList.size());
for (SeckillGood seckillGood : seckillGoodList) {
goodIds.add(seckillGood.getId());
}
// 3.通过 feign 远程调用商品服务,查询列表
// TODO 商品服务的远程调用,通过 feign 暴露的接口
Result<List<Good>> result = goodFeignApi.getGoodListByIds(goodIds);
// 商品服务可能返回三种结果:1. 正常返回; 2. 降级的null;3. code != 200 返回result
if (result.hasError()) { // result == null || result.getCode() != 200 的情况是有错误
// 返回 null 或者抛出异常给调用者响应的提示
throw new BusinessException(SeckillServerCodeMsg.DEFAULT_ERROR);
}
//正常调用返回的数据
List<Good> goodList = result.getData();
// 通过遍历的方式来给属性赋值,效率低。采用HashMap做缓存的方式来操作
HashMap<Long, Good> tempCache = new HashMap<>(goodList.size());
// 把查到的列表先丢到map中
for (Good good : goodList) {
tempCache.put(good.getId(), good);
}
// 4.遍历商品列表,将列表中每项的属性,复制到 VO 中,得到 vo 列表
List<SeckillGoodVO> seckillGoodVOList = new ArrayList<>(seckillGoodList.size());
for (SeckillGood seckillGood : seckillGoodList) {
Good good = tempCache.get(seckillGood.getGoodId());// 拿到map中的值
SeckillGoodVO vo = new SeckillGoodVO(); // 创建vo对象,复制两者属性
// ------ 注意属性复制的顺序:vo的id应该和秒杀商品的id相同 -------
// * 复制商品的属性
if (good != null)
//有值才复制属性:BeanUtils
BeanUtils.copyProperties(good, vo);
// * 复制秒杀商品的属性
BeanUtils.copyProperties(seckillGood, vo);
seckillGoodVOList.add(vo); // 添加进list中
}
return seckillGoodVOList;
}
}
降级有效:需要开启,默认feign降级是关闭的。
测试:启动服务查看秒杀商品列表信息是否完整:
商品详情:
验证登录时间的刷新:
详情需要登录,cookie有效时间刷新。之前的实现登录,每一次登录都生成了新的key存进Redis中,cookie也是全新的。这样不合理,我们希望:在设置等时效30分钟之内,只要在页面中做了任意操作,都将key和cookie的时效延长,刷新成30分钟。这样就不用在cookie或者key到达失效时间,而需要重新登录。动态的根据用户操作调整时效性。
要在多个微服务之间都可以刷新时间,在Zuul网关中利用它的过滤器实现:
流程:
zuul-server网关服务来实现:
zuul网关中不做业务处理,刷新时间,要在member-server中实现,需要远程调用;引入member-api依赖;
将CookieUtil提到member-api中:添加一个获取cookieValue的方法:
public abstract class CookieUtils {
private static final String DOMAIN = "localhost";
private static final String PATH = "/";
private static final Integer TIME = 30 * 60;
public static final String USERTOKEN_NAME = "userToken";
// 将数据添加进cookie中
public static void addInCookie(HttpServletResponse response, String cookieName, String cookieVal) {
Cookie cookie = new Cookie(cookieName, cookieVal);
cookie.setMaxAge(TIME);
cookie.setDomain(DOMAIN);
cookie.setPath(PATH);
//共享cookie
response.addCookie(cookie);
}
// 获取 cookie 的值
public static String getCookieValue(HttpServletRequest request, String userToken) {
Cookie[] cookies = request.getCookies();
if (cookies == null || cookies.length == 0) {
return null;
}
for (Cookie cookie : cookies) {
if (cookie.getName().equals(userToken)) {
return cookie.getValue();
}
}
return null;
}
}
会员服务暴露的接口:
member-server中:
刷新方法:
// 刷新 token 时间
public Boolean refeshToken(String token) {
// Redis:调用 expire() 方法
Boolean expire = redisTemplate.expire(
RedisKeys.USER_LOGIN_TOKEN.join(token),
Consts.USER_INFO_TOKEN_VAL_TIME,
TimeUnit.MINUTES);
// 只要有 key 就不会失败,没有 key 都是失败的
return expire;
}
zuul-server中filter代码:
public class TokenRefreshFilter extends ZuulFilter {
private UserFeignApi userFeignApi; //注入userFeignApi远程调用
public String filterType() {
return FilterConstants.POST_TYPE; // 过滤类型
}
public int filterOrder() {
return 0; //过滤链优先级
}
public boolean shouldFilter() {
// 判断时候有 cookie ,在判断 cookie 中是否有 token
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String token = CookieUtils.getCookieValue(request, CookieUtils.USERTOKEN_NAME);
return !StringUtils.isEmpty(token); // token 不为空的时候才放行
}
public Object run() throws ZuulException {
// 执行 token 刷新操作
// 先通过上下文得到 值 token
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String token = CookieUtils.getCookieValue(request, CookieUtils.USERTOKEN_NAME);
// 远程调用会员服务方法,完成token的刷新
Result<Boolean> result = userFeignApi.refeshToken(token);
if (!result.hasError()) { //也是返回有三种情况
// 刷新Cookie有效时间————》重新设置Cookie即可
CookieUtils.addInCookie(ctx.getResponse(), CookieUtils.USERTOKEN_NAME, token);
}
return null;
}
}
测试:重新登录不会再生成新的cookie和key
Zuul网关拦截的使用场景:
一:特点:
路由+过滤器=Zuul
核心为一系列的过滤器
二:前置过滤器(Pre)作用:
1.限流(流量过大时,依据某种规则把请求挡回去,后续的逻辑就不在处理了)
2.鉴权(如果发现没有访问权限,直接就拦截了 )
3.参数检验调整
三:后置过滤器(Post)
1.统计
2.日志
四:异常处理器(error)
一般会在error类型和post类型过滤器中结合来处理。
服务调用时长统计:pre和post结合使用。
小伙砸,欢迎再看分享给其他小伙伴!共同进步!
本文分享自微信公众号 - java学途(javaxty)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
来源:oschina
链接:https://my.oschina.net/u/4673060/blog/4699242