1.数据库设计

创建Mysql数据库

使用Mysql5.7.26版本,创建数据库ShoppingSystem,并设置编码为utf8

数据库E-R图设计

  • 文本列出所需要的表与表的基本信息
表名 中文含义 介绍
Category 分类表 存放分类信息(分类名:电子元件,电器,书籍)
Property 属性表 存放属性信息(属性名:颜色,重量,品牌)
Product 产品表 存放产品信息(产品名,价格等)
PropertyValue 属性值表 存放属性值信息(产品相应属性表的值)
ProductImage 产品图片表 存放产品图片信息
Review 评论表 存放评论信息(用户对某一产品的评论)
User 用户表 存放用户信息
Order 订单表 存放订单信息(用户下订单基本信息:邮寄地址,电话号码)
OrderItem 订单项表 存放订单项信息(订单产品信息:产品种类,数量)
  • 表与表的关系
Category-分类 Product-产品
Category-分类 Property-属性
Property-属性 PropertyValue-属性值
Product-产品 PropertyValue-属性值
Product-产品 ProductImage-产品图片
Product-产品 Review-评价
User-用户 Order-订单
Product-产品 OrderItem-订单项
User-用户 OrderItem-订单项
Order-订单 OrderItem-订单项
User-用户 Review-评价
  • 数据库设计E-R图(这里用idea直接生成)

image-20211214154107335

重要数据表解析

产品表(prouct)

name: 产品名称
subTitle: 小标题
originalPrice: 原始价格
promotePrice: 优惠价格
stock: 库存
createDate: 创建日期

订单表(order)

orderCode: 订单号
address:收货地址
post: 邮编
receiver: 收货人信息
mobile: 手机号码
userMessage: 用户备注信息
createDate: 订单创建日期
payDate: 支付日期
deliveryDate: 发货日期
confirmDate:确认收货日期
status: 订单状态
外键uid,指向用户表id字段

订单项表(orderitem)

外键pid,指向产品表id字段
外键oid,指向订单表id字段
外键uid,指向用户表id字段
number字段表示购买数量

2.项目结构

系统访问路径

前台:http://127.0.0.1:8080/tmall_springboot/home

后台:http://127.0.0.1:8080/tmall_springboot/admin

技术架构

前端:

  • Vue.js:前端逻辑处理数据
  • Bootstrap:使用模板样式
  • Jquery
  • axios
  • Thymeleaf:主要使用其HTML包含技术,整合页面共用部分(Springboot官方推荐的视图)

后端:

  • SpringBoot 1.5.9 RELEASE
  • Maven
  • Hibernate

相关依赖准备

pom.xml文件导入相关依赖

<dependencies><!-- springboot web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- springboot tomcat 支持 --><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-devtools</artifactId><optional>true</optional></dependency><!-- jpa:java持久层api,用于操作数据库--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency>    <!-- redis:基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,一般说来,会被当作缓存使用。 因为它比数据库(mysql)快 -->      <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>   <!-- springboot test --> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency> <!-- thymeleaf: Thymeleaf 是一种模板语言,可以达到和JSP一样的效果,但是比起JSP 对于前端测试更加友好--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><!-- elastic search:Elasticsearch是一个基于Lucene库的搜索引擎。它提供了一个分布式、支持多租户的全文搜索引擎 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId></dependency><!-- 用了 elasticsearch 就要加这么一个,不然要com.sun.jna.Native 错误 --><dependency><groupId>com.sun.jna</groupId><artifactId>jna</artifactId><version>3.0.9</version></dependency>        <!-- thymeleaf legacyhtml5 模式支持 -->      <dependency><groupId>net.sourceforge.nekohtml</groupId><artifactId>nekohtml</artifactId><version>1.9.22</version></dependency>   <!-- 测试支持 --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency> <!-- tomcat的支持.--><dependency><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-jasper</artifactId><version>8.5.23</version></dependency><!-- mysql:数据库支持--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.21</version></dependency><!-- junit:java自动测试工具 --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version> 4.12</version></dependency>  <!-- commons-lang:提供常用工具包 --><dependency><groupId>commons-lang</groupId><artifactId>commons-lang</artifactId><version>2.6</version></dependency>       <!-- shiro:Java 当下常见的安全框架,主要用于用户验证和授权操作 --><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.3.2</version></dependency>  <!-- hsqldb是一款Java内置的数据库,非常适合在用于快速的测试和演示的Java程序中 --><dependency><groupId>org.hsqldb</groupId><artifactId>hsqldb</artifactId></dependency><!-- springfox-swagger依赖添加:文档化工具 --><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>20.0</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version></dependency><dependency><groupId>com.github.xiaoymin</groupId><artifactId>swagger-bootstrap-ui</artifactId><version>1.9.6</version></dependency></dependencies>

3.技术亮点

分页组接口开发

(1)简单分页方法

Service层实现简单分页方法

这里使用JPA提供的Pageable类型对列表进行分页

Pageable是从0开始计算页数的,所以这里需要pageNum - 1

public Page<Category> getpage(int pageNum, int pageLimit){Pageable pageable = new PageRequest(pageNum - 1 , pageLimit);return categoryDAO.findAll(pageable);}

Controller层调用分页方法

通过@RequestParam设置从前台get方法发来的page和size信息

@GetMapping("/catepage")
public Page<Category> pageList(@RequestParam(value = "page", defaultValue = "1") int page ,@RequestParam(value = "size", defaultValue = "5") int size)throws Exception{return categoryService.getpage(page, size);}

测试结果

访问请求链接:http://localhost:8080/shopping_system/catepage?page=2&size=5

image-20211215173655433

(2)分页组类

分页功能进阶封装

JPA提供的分页类可以返回分割后的列表内容和分类信息如总共数据数(totalElements),总共分割的页面(totalPages)与当前访问的页面(number),但是这些数据不能方便提供一个方便的接口让前端实现部分分页节点展示分页节点遍历

当前是第8页,前面要显示3个,后面要显示3个,总共7条分页点,Pageable默认就不提供了,即Pageable无法实现根据当前选择页调整接口返回的数据,而只能硬性分页

所以我们需要做了一个 PageNavigator, 首先对 Page 类进行了封装,然后在构造方法里提供了一个 navigatePages 参数作为区间分页节点数

在构造方法里,还调用了 calcNavigatepageNums, 就是用来计算这个数值,并返回到一个int 数组变量 navigatepageNums ,方便前端遍历展示,而这个数组的大小为navigatePages

public class PageNavigator<T> {// 引用Page类Page<T> pageFromJPA;int totalPages;int number;long totalElements;int size;// 单页数据数int numberOfElements;// 分页数据List<T> contents;// 是否为首尾判断boolean first;boolean last;// 是否有数据boolean isHasContent;// 是否有前驱boolean isHasPrevious;// 是否有后续boolean isHasNext;// 规定区间分页节点数int navigatePages;// 规定区间分页节点列表int[] navigatepageNums;// 无参构造函数public PageNavigator(){}// 构造规定分页区间大小的分页函数public PageNavigator(Page<T> pageFromJPA, int navigatePages){// 引用Page里面的成员变量this.pageFromJPA = pageFromJPA;this.navigatePages = navigatePages;totalPages = pageFromJPA.getTotalPages();number = pageFromJPA.getNumber();totalElements = pageFromJPA.getTotalElements();size = pageFromJPA.getNumberOfElements();contents = pageFromJPA.getContent();isHasContent = pageFromJPA.hasContent();first = pageFromJPA.isFirst();last = pageFromJPA.isLast();isHasNext = pageFromJPA.hasNext();isHasPrevious = pageFromJPA.hasPrevious();}// 计算出分页节点列表private void calcNavigatepageNums(){int[] navigatepageNums;// 总页数int totalPages = getTotalPages();// 当前页int num = getNumber();// 总页数小于区间分页节点数if(totalPages <= navigatePages){navigatepageNums = new int[totalPages];for(int i = 0; i < totalPages; i++){navigatepageNums[i] = i + 1;}}else{navigatepageNums = new int[ navigatePages];// 计算区间列表首尾索引int startNum = num - navigatePages / 2;int endNum = 0;if(navigatePages % 2 == 0){endNum = num + navigatePages / 2 - 1;}else{endNum = num + navigatePages / 2;}// 首navigatePages页if(startNum < 0){startNum = 1;for(int i = 0; i < navigatePages; i++){navigatepageNums[i] = startNum++;}}// 尾navigatePages页else if(startNum > navigatePages){endNum = totalPages;for(int i = navigatePages - 1; i >= 0; i--){navigatepageNums[i] = endNum--;}}// 中间navigatePages页else{for(int i = 0; i < navigatePages; i++){navigatepageNums[i] = startNum++;}}}this.navigatepageNums = navigatepageNums;}// 成员变量对应的Getter与Setterpublic int getTotalPages() {return totalPages;}public void setTotalPages(int totalPages) {this.totalPages = totalPages;}public int getNumber() {return number;}public void setNumber(int number) {this.number = number;}public long getTotalElements() {return totalElements;}public void setTotalElements(long totalElements) {this.totalElements = totalElements;}public int getSize() {return size;}public void setSize(int size) {this.size = size;}public int getNumberOfElements() {return numberOfElements;}public void setNumberOfElements(int numberOfElements) {this.numberOfElements = numberOfElements;}public List<T> getContents() {return contents;}public void setContents(List<T> contents) {this.contents = contents;}public boolean isFirst() {return first;}public void setFirst(boolean first) {this.first = first;}public boolean isLast() {return last;}public void setLast(boolean last) {this.last = last;}public boolean isHasContent() {return isHasContent;}public void setHasContent(boolean hasContent) {isHasContent = hasContent;}public boolean isHasPrevious() {return isHasPrevious;}public void setHasPrevious(boolean hasPrevious) {isHasPrevious = hasPrevious;}public boolean isHasNext() {return isHasNext;}public void setHasNext(boolean hasNext) {isHasNext = hasNext;}public int getNavigatePages() {return navigatePages;}public void setNavigatePages(int navigatePages) {this.navigatePages = navigatePages;}public int[] getNavigatepageNums() {return navigatepageNums;}public void setNavigatepageNums(int[] navigatepageNums) {this.navigatepageNums = navigatepageNums;}}

Service层实现进阶分页方法

    public PageNavigator<Category> getpage(int page, int size, int navigatePages){Sort sort = new Sort(Sort.Direction.DESC, "id");Pageable pageable = new PageRequest(page, size, sort);Page pageFrom = categoryDAO.findAll(pageable);return new PageNavigator<>(pageFrom, navigatePages);}

Controller层调用进阶分页方法

    @GetMapping("/catepage")public PageNavigator<Category> pageList(@RequestParam(value = "page", defaultValue = "1") int page,@RequestParam(value = "size", defaultValue = "5") int size)throws Exception{// 接口初始页调整为从1开始page = page < 1 ? 1 : page;PageNavigator<Category> list = categoryService.getpage(page - 1, size, 5);return list;}

测试结果

访问地址:http://localhost:8080/tmall_springboot/categories?start=3&size=2

可以看到最终实现了提供一个存储5个页面索引的数组

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EmiVldLN-1651665266478)(https://cdn.jsdelivr.net/gh/Autovy/Image/img/202202282106562.png)]

(3)分页方法比较

JPA提供的分页类——Page可以满足各种分页需求,大部分时候用它就足够了,但是Pageable无法实现根据当前选择页调整接口返回的数据,而只能硬性分页即 页数(totalPage) = 数据数(totalElements) / 页大小(size)

表现在前端所有的分页都在一组分页栏中,如果想部分显示分页栏就需要前端去定制分页分组方法

image-20211215173655433

但是如果前端有需求让后端根据当前选择页,以当前页为中点返回n个页面为一组的索引供前端调用

这时候我们就要对Page类进行封装,构造一个分页组类,在构造方法中提供一个navigatePages参数(分页组大小),并提供calNavigateNums方法根据当前页计算出分到同一组的页面索引并存储到数组navigatepageNums中供前端遍历展示

表现在前端可以通过接口获得当前页同一组分页的索引方便遍历

Springboot注解补充

实体类中,@Transient注解的字段,是不与数据库映射的,可以额外添加到接口的字段即该字段不参与自动关联中的sql查询

这些字段可以用来存储:通过查询数据库得到的列表(不用另外建集合对象存储),需要经过计算的数据(也可以放在数据库),数据状态(也可以放在数据库)

订单表@Transient注解字段,在服务层进行赋值操作

// 订单项列表
@Transient
private List<OrderItem> orderItems;
// 订单总金额
@Transient
private float total;
// 订单物品总数量
@Transient
private int totalNumber;
// 订单状态
@Transient
private String statusDesc;

使用

@ManyToOne
@JoinColumn(name=“pid”)

可以标注关系,就可以使用JPA的findBy等方法如:findByProductOrderByIdDesc

// 一个产品有多个属性值
@ManyToOne
@JoinColumn(name="pid")
private Product product;// 一个属性有多个属性值(属性 + 产品决定一条属性值)
@ManyToOne
@JoinColumn(name="ptid")
private Property property;

数据库设计:多对多关系

在实际应用中,多对多关系会分解为两个一对多的关系

属性值由产品和属性共同决定

// 一个产品,有多个属性值(不同属性,同一产品)
@ManyToOne
@JoinColumn(name="pid")
private Product product;// 一个属性有多个属性值(不同产品,同一属性)
@ManyToOne
@JoinColumn(name="ptid")
private Property property;

订单项由订单,用户,产品共同决定

// 一个产品可以有多个订单项(不同用户/不同订单,同一产品)
@ManyToOne
@JoinColumn(name="pid")
private Product product;// 一个订单可以有多个订单项(不同产品/不同用户,同一订单)
@ManyToOne
@JoinColumn(name="oid")
private Order order;// 一个用户可以有多个订单项(不同产品/不同订单,同一用户)
@ManyToOne
@JoinColumn(name="uid")
private User user;

在review类中的内对象如:prouct,user由于一对多的关联,在数据库中映射为pid,uid字段)

所以说JPA是一个ORM框架,对象和数据库无缝衔接

循环依赖解决方案

在SpringBoot + JPA的架构中,容易出现循环依赖问题,一般会出现在一对多的场景下,总结来说是一对多实体中都要引用对方来维持OnetoMany的关系,所以极容易出现循环依赖:(

(1)经典场景

订单项中引用订单,以构成多对一关系(可以使用订单id查到订单项)

// 一个订单可以有多个订单项(不同产品/不同用户,同一订单)
@ManyToOne
@JoinColumn(name="oid")
private Order order;

订单中引用订单项存储在集合中,用来存储从数据库查询来的结构(往往是因为要利用这些字段进行计算)

// 订单项列表
@Transient
private List<OrderItem> orderItems;
// 订单总金额
@Transient
private float total;
// 订单物品总数量
@Transient
private int totalNumber;

这样的结构就是循环依赖,导致数据重复加载,因为orderItems要调用方法填充,所以会为空(一般情况下会栈溢出)最终造成的数据是:Order含有orderItems,orderItems含有Order,Order的orderItem列表为空,所以这里的Order重复了一次

(2)方案一:@JsonBackReference注解

JsonBackReference注解用在一(一对多的一)的一方,可以阻止其被序列化,前提是对应的接口不需要调用到它,而只是需要用它来查询

如:一个产品有多张图片,我们不需要在图片列表接口使用到产品信息,而只是需要用产品id查询其图片

产品类

@Transient
// 产品首图
private ProductImage firstProductImage;
@Transient
private List<ProductImage> productSingleImages;
@Transient
private List<ProductImage> productDetailImages;

产品图片类

@ManyToOne
@JoinColumn(name="pid")
@JsonBackReference
private Product product;

缺点

  • 关系是双向的,使用了JsonBackReference,就无法使用根据图片找到其属于的产品的方法,只能单方向查询即根据产品查找到其图片列表
  • JsonBackReference标记的字段与Redis的整合会有冲突
(3)方案二:及时清除法

在服务层定义清除方法,在控制层调用

 // Orderitem中有Order字段,标注多对一关系// Order中有Orderitem列表,用于存储订单项列表// Order中有Orderitem列表,而Orderitem中又有Order字段,产生无穷的递归// 所以这里需要设置Orderitem的Order设为空public void removeOrderFromOrderItem(List <Order> orders) {for (Order order : orders) {removeOrderFromOrderItem(order);}}public void removeOrderFromOrderItem(Order order) {List<OrderItem> orderItems= order.getOrderItems();for (OrderItem orderItem : orderItems) {orderItem.setOrder(null);}}
// 填充Order的orderItem列表
orderItemService.fill(page.getContent());
// 清除orderItem中的Order字段
orderService.removeOrderFromOrderItem(page.getContent());

拦截器

拦截前端某些没有权限的访问,如没有登录权限的用户访问个人信息表,跳转到登录页

(1)拦截器
package com.how2java.tmall.interceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;import org.apache.commons.lang.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;// 登录拦截器,用于拦截未登录情况下的访问
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {HttpSession session = httpServletRequest.getSession();String contextPath=session.getServletContext().getContextPath();// 需要验证登录的页面String[] requireAuthPages = new String[]{"buy","alipay","payed","cart","bought","confirmPay","orderConfirmed","forebuyone","forebuy","foreaddCart","forecart","forechangeOrderItem","foredeleteOrderItem","forecreateOrder","forepayed","forebought","foreconfirmPay","foreorderConfirmed","foredeleteOrder","forereview","foredoreview"};// 获取uriString uri = httpServletRequest.getRequestURI();//移除前缀/tmall_springbooturi = StringUtils.remove(uri, contextPath+"/");String page = uri;// 判断链接名,是否以验证登录数组里的开头if(begingWith(page, requireAuthPages)){Subject subject = SecurityUtils.getSubject();// 如果是则跳转到login页面if(!subject.isAuthenticated()) {httpServletResponse.sendRedirect("login");return false;}}return true;   }private boolean begingWith(String page, String[] requiredAuthPages) {boolean result = false;for (String requiredAuthPage : requiredAuthPages) {if(StringUtils.startsWith(page, requiredAuthPage)) {result = true; break;}}return result;}@Overridepublic void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {}@Overridepublic void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {}
}

通过实现SpringMCV的HandlerInterceptor来实现拦截器,其中包含3个方法:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handle)

该方法将在请求处理之前进行调用。SpringMVC中的Interceptor是链式的调用的,在一个应用中或者说是在一个请求中可以同时存在多个Interceptor 。

每个Interceptor的调用会依据它的声明顺序依次执行,而且最先执行的都是Interceptor中的preHandle方法,所以可以在这个方法中进行一些前置初始化操作或者是对当前请求的一个预处理,也可以在这个方法中进行一些判断来决定请求是否要继续进行下去。

该方法的返回值是布尔值Boolean类型的,当它返回为false 时,表示请求结束,后续的Interceptor和Controller都不会再执行;

当返回值为true时就会继续调用下一个Interceptor的preHandle方法,如果已经是最后一个Interceptor的时候就会是调用当前请求的Controller方法

postHandle(HttpServletRequest request, HttpServletResponse response, Object handle, ModelAndView modelAndView)
由preHandle方法的解释我们知道这个方法包括后面要说到的afterCompletion方法都只能是在当前所属的Interceptor的preHandle方法的返回值为true时才能被调用

postHandle方法,顾名思义就是在当前请求进行处理之后,也就是Controller方法调用之后执行,
但是它会在DispatcherServlet进行视图返回渲染之前被调用,所以我们可以在这个方法中对Controller处理之后的ModelAndView对象进行操作。

postHandle方法被调用的方向跟preHandle是相反的,也就是说先声明的Interceptor 的postHandle方法反而会后执行,这和Struts2里面的Interceptor 的执行过程有点类型。Struts2 里面的Interceptor 的执行过程也是链式的,只是在Struts2 里面需要手动调用ActionInvocation 的invoke 方法来触发对下一个Interceptor 或者是Action 的调用,然后每一个Interceptor 中在invoke 方法调用之前的内容都是按照声明顺序执行的,而invoke 方法之后的内容就是反向的

afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex)
该方法也是需要当前对应的Interceptor 的preHandle 方法的返回值为true 时才会执行。

顾名思义,该方法将在整个请求结束之后,也就是在DispatcherServlet 渲染了对应的视图之后执行。
这个方法的主要作用是用于进行资源清理工作的。

(2)拦截器配置
// 访问拦截器配置
package com.how2java.tmall.config;import com.how2java.tmall.interceptor.LoginInterceptor;
import com.how2java.tmall.interceptor.OtherInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;@Configuration
// 拦截器的配置
class WebMvcConfigurer extends WebMvcConfigurerAdapter{@Beanpublic LoginInterceptor getLoginIntercepter() {return new LoginInterceptor();}}

Shiro登录验证

由于本项目仅仅有用户一个权限,所以只需要判断用户是否登录,并不需要比较细粒度的权限分配

(1)JPARealm验证授权器

Shiro与用户之间的中介,为Shiro提供验证和授权用户的方法

package com.how2java.tmall.realm;import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;import com.how2java.tmall.pojo.User;
import com.how2java.tmall.service.UserService;// 通过JPA进行验证授权
// (相当于一个中介,拿着用户信息去数据库找用户拥有的角色和权限)
// 将Realm提供给Shiro,由其负责调用,不需要直接调用
public class JPARealm extends AuthorizingRealm {@Autowiredprivate UserService userService;// 认证:查询用户身份与密码,解决你是谁的问题@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {// 从token中取出用户名称String userName = token.getPrincipal().toString();// 查询用户表得到用户加密密码User user = userService.getByName(userName);String passwordInDB = user.getPassword();// 获得用户表中的盐String salt = user.getSalt();// 以用户名,加密密码,盐,真实信息,真正姓名作为认证信息SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName, passwordInDB, ByteSource.Util.bytes(salt),getName());return authenticationInfo;}// 授权:赋予用户权限,解决你能做什么的问题@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {//SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();return s;}}
(2)Shiro配置
package com.how2java.tmall.config;import com.how2java.tmall.realm.JPARealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;// Shiro配置文件
@Configuration
public class ShiroConfiguration {@Beanpublic static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {return new LifecycleBeanPostProcessor();}// 过滤器,实现对请求的拦截和跳转@Beanpublic ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){// 创建 ShiroFilterFactoryBean 对象ShiroFilterFactoryBean shiroFilterFactoryBean  = new ShiroFilterFactoryBean();// 设置SecurityManagershiroFilterFactoryBean.setSecurityManager(securityManager);/*这里可以设置URL并为它们配置权限,本项目没有用到*/return shiroFilterFactoryBean;}// shiro核心组件@Beanpublic SecurityManager securityManager(){// 创建DefaultWebSecurityManager对象DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();// 设置其使用的RealmsecurityManager.setRealm(getJPARealm());return securityManager;}// 加载身份认证与授权模块@Beanpublic JPARealm getJPARealm(){JPARealm myShiroRealm = new JPARealm();myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());return myShiroRealm;}// 指定使用md5加密算法,并进行两次加密@Beanpublic HashedCredentialsMatcher hashedCredentialsMatcher(){HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();hashedCredentialsMatcher.setHashAlgorithmName("md5");hashedCredentialsMatcher.setHashIterations(2);return hashedCredentialsMatcher;}/***  开启shiro aop注解支持.*  使用代理方式;所以需要开启代码支持;* @param securityManager* @return*/@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);return authorizationAttributeSourceAdvisor;}
}
(3)注册接口

Realm的验证需要对应注册里的加密方法即md5 * 2 + 盐

    // 注册接口@PostMapping("/foreregister")public Object register(@RequestBody User user) {String name =  user.getName();String password = user.getPassword();// 对姓名中的特殊符号进行转义name = HtmlUtils.htmlEscape(name);user.setName(name);// 判断用户名是否存在boolean exist = userService.isExist(name);if(exist){String message ="用户名已经被使用,不能使用";return Result.fail(message);}// 随机生成盐String salt = new SecureRandomNumberGenerator().nextBytes().toString();int times = 2;// 采用md5加密String algorithmName = "md5";// md5 + 盐对用户密码进行加密得到加密密码// times = 2,表明进行两次的md5加密String encodedPassword = new SimpleHash(algorithmName, password, salt, times).toString();// 将盐和加密密码存入数据库中user.setSalt(salt);user.setPassword(encodedPassword);userService.add(user);return Result.success();}
(4)登录接口

配置好Shiro后,登录验证时可以快速使用啦!

// 登录接口@PostMapping("/forelogin")public Object login(@RequestBody User userParam, HttpSession session) {String name =  userParam.getName();name = HtmlUtils.htmlEscape(name);// shiro认证登录(你是谁?)// subject指的是:"当前正在执行的用户的特定的安全视图"// 可以把Subject看成是shiro的"User"概念Subject subject = SecurityUtils.getSubject();UsernamePasswordToken token = new UsernamePasswordToken(name, userParam.getPassword());try {subject.login(token);User user = userService.getByName(name);// 将user存储进seesion中,后续可以随时取出用于验证登录session.setAttribute("user", user);return Result.success();} catch (AuthenticationException e) {String message ="账号密码错误";return Result.fail(message);}}

Redis缓存

(1)Redis可视化工具

推荐使用RedisClient,数据一般都在db0中

(2)Redis配置类

该缓存配置类主要是使redis内的key和value转换为可读性的字符串

@Configuration
//Redis 缓存配置类public class RedisConfig extends CachingConfigurerSupport { @Beanpublic CacheManager cacheManager(RedisTemplate<?,?> redisTemplate) {RedisSerializer stringSerializer = new StringRedisSerializer();Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.PUBLIC_ONLY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);redisTemplate.setKeySerializer(stringSerializer);redisTemplate.setHashKeySerializer(stringSerializer);  redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);         redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);CacheManager cacheManager = new RedisCacheManager(redisTemplate);return cacheManager;}
}
(3)Redis配置文件
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=10
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=0
(4)缓存的启用
// 系统启动入口
@SpringBootApplication
// 启动缓存
@EnableCaching
@EnableElasticsearchRepositories(basePackages = "com.how2java.tmall.es")
@EnableJpaRepositories(basePackages = {"com.how2java.tmall.dao", "com.how2java.tmall.pojo"})
public class Application {static {// 检测端口上的服务是否启动PortUtil.checkPort(6379,"Redis 服务端",true);PortUtil.checkPort(9300,"ElasticSearch 服务端",true);PortUtil.checkPort(5601,"Kibana 工具", true);}public static void main(String[] args) {SpringApplication.run(Application.class, args);}
}

这里的PortUtil是一个检测端口上服务是否运行的简单工具类,如下


package com.how2java.tmall.util;import java.io.IOException;
import java.net.ServerSocket;import javax.swing.JOptionPane;// 工具类,检查某个端口对应的服务是否启动
// 可以用于检查redis服务和es服务
public class PortUtil {public static boolean testPort(int port) {try {ServerSocket ss = new ServerSocket(port);ss.close();return false;} catch (java.net.BindException e) {return true;} catch (IOException e) {return true;}}public static void checkPort(int port, String server, boolean shutdown) {if(!testPort(port)) {if(shutdown) {String message =String.format("在端口 %d 未检查得到 %s 启动%n",port,server);JOptionPane.showMessageDialog(null, message);System.exit(1);}else {String message =String.format("在端口 %d 未检查得到 %s 启动%n,是否继续?",port,server);if(JOptionPane.OK_OPTION !=    JOptionPane.showConfirmDialog(null, message)) System.exit(1);}}}}
(5)缓存的使用

缓存的使用一般在服务层使用

有序集合管理

通过在服务层中注解@CacheConfig,创建一个有序集合类型的缓存,管理该服务下所有的keys

// 分类服务层
@Service
// redis缓存一般都在服务层进行操作
// 分类服务下的所有keys都由categories来管理(数据存储与categories是平行关系)
@CacheConfig(cacheNames="categories")
public class CategoryService {.....
}

查询插入缓存

 // 获得单条分类服务// 添加一条缓存到redis中,以categories-one- + 参数id为key值// 存储的主要数据为Category对象@Cacheable(key="'categories-one-'+ #p0")public Category get(int id) {Category c= categoryDAO.findOne(id);return c;}// 列出单页分类表(提供分页组索引)// 添加一条缓存到redis中,以categories-page- + 参数start + 参数size 为key值// 存储的主要数据为Page4Navigator<Category>数组@Cacheable(key="'categories-page-'+#p0+ '-' + #p1")public Page4Navigator<Category> list(int start, int size, int navigatePages) {Sort sort = new Sort(Sort.Direction.DESC, "id");Pageable pageable = new PageRequest(start, size, sort);Page pageFromJPA =categoryDAO.findAll(pageable);return new Page4Navigator<>(pageFromJPA,navigatePages);}

返回的java对象或集合都会变成JSON字符串

更新删除缓存

准确来说是插入,删除,更新删除缓存以保持数据一致性

使用@CacheEvict(allEntries=true)删除category~keys的所有keys

 // 增加删除更新时// 增加分类服务@CacheEvict(allEntries=true)public void add(Category bean) {categoryDAO.save(bean);}// 删除分类服务@CacheEvict(allEntries=true)public void delete(int id) {categoryDAO.delete(id);}// 更新分类服务@CacheEvict(allEntries=true)public void update(Category bean) {categoryDAO.save(bean);}

内部使用缓存

因为Springboot的缓存机制是通过切面编程aop来实现,从fill方法中调用listByCategory即内部调用,aop是拦截不到的,自然不会走缓存,这里我们可以通过SpringContextUtil工具类诱发aop

// 填充分类中的产品集合
public void fill(Category category) {// 通过SpringContextUtil调用listByCategory上的缓存方法// 即 @Cacheable(key="'products-cid-'+ #p0.id")// 这样在方法内部的查询也能够使用缓存ProductService productService = SpringContextUtil.getBean(ProductService.class);List<Product> products = productService.listByCategory(category);productImageService.setFirstProdutImages(products);category.setProducts(products);
}

SpringContextUtil工具类诱发aop

package com.how2java.tmall.util;import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;@Component
public class SpringContextUtil implements ApplicationContextAware {private SpringContextUtil() {}private static ApplicationContext applicationContext;@Overridepublic void setApplicationContext(ApplicationContext applicationContext){SpringContextUtil.applicationContext = applicationContext;}public static <T> T getBean(Class<T> clazz) {return applicationContext.getBean(clazz);}}

Elasticsearch

Elasticsearch也是基于Lucene的全文检索库,本质也是存储数据,很多概念与关系型数据库是一致的,如下对照

通过es搜索引擎我们可以更快地查找到数据

(1)ES可视化

kibana是es的可视化工具,开启后可以通过访问 http://127.0.0.1:5601/ 查看kibana页面

(2)配置ES
# ElasticSearch
spring.data.elasticsearch.cluster-nodes = 127.0.0.1:9300
(3)ES注解实体类
// @Document注解Category实体类,一个Category对象即为一个Document(相当于数据库的一行)
// 连接到es的tmall_springboot索引(相当于数据库),produt类(相当于表)上
@Document(indexName = "tmall_springboot",type = "product")
(4)esDAO的创建

由于整合了ES的JPA和操作数据库使用的JPA有冲突,所以不能放在同一个包下

package com.how2java.tmall.es;import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;import com.how2java.tmall.pojo.Product;
// 用于链接es的DAO
// esDAO和其他DAO不能放在一个包下否则会启动异常
// 主要使用es实现对产品的模糊查询
public interface ProductESDAO extends ElasticsearchRepository<Product,Integer>{}
(5)Application引入ES
// esJPA引入
@EnableElasticsearchRepositories(basePackages = "com.how2java.tmall.es")// JPA引入
@EnableJpaRepositories(basePackages = {"com.how2java.tmall.dao", "com.how2java.tmall.pojo"})
(6)服务层同步ES

增删改操作

增删改操作的数据需要同步ES和数据库

 // 通过ProductDAO对数据库有影响的// 都要通过productESDAO同步到es@CacheEvict(allEntries=true)public void add(Product bean) {productDAO.save(bean);productESDAO.save(bean);}@CacheEvict(allEntries=true)public void delete(int id) {productDAO.delete(id);productESDAO.delete(id);}@CacheEvict(allEntries=true)public void update(Product bean) {productDAO.save(bean);productESDAO.save(bean);}

ES初始化

ES内数据为空,就将数据库中的数据同步到es

 // 初始化数据到esprivate void initDatabase2ES() {Pageable pageable = new PageRequest(0, 5);Page<Product> page =productESDAO.findAll(pageable);// 查询es中是否有数据if(page.getContent().isEmpty()) {// 如果数据为空,将数据从数据库同步到es中List<Product> products= productDAO.findAll();for (Product product : products) {productESDAO.save(product);}}}
(7)服务层查询ES
 // 通过es进行查询public List<Product> search(String keyword, int start, int size) {// 初始化esinitDatabase2ES();// QueryBuilders提供了大量静态方法,用于生成各种不同类型的查询对象// 构建查询条件(多条件查询)FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery()// 为提供的字段名和文本创建一个通用查询.add(QueryBuilders.matchPhraseQuery("name", keyword),ScoreFunctionBuilders.weightFactorFunction(100))// 设置权重分为求和模式.scoreMode("sum")// 设置权重分最低分.setMinScore(10);// 设置分页参数Sort sort  = new Sort(Sort.Direction.DESC,"id");Pageable pageable = new PageRequest(start, size,sort);// 添加分页参数和查询条件SearchQuery searchQuery = new NativeSearchQueryBuilder().withPageable(pageable).withQuery(functionScoreQueryBuilder).build();// 执行查询获取结果Page<Product> page = productESDAO.search(searchQuery);// 返回结果return page.getContent();}

参考资料

Spring Data Elasticsearch基本使用

史上最全面的Elasticsearch使用指南

Spring data jpa中实体关系解决方案

Spring Data JPA 使用详解

天猫仿站秒杀系统开发相关推荐

  1. dedecms织梦仿站二次开发标签大全

    为了方便一些织梦爱好者对织梦的网站开发和网站仿制我们特意准备一个织梦标签生成器 织梦标签生成器链接:https://www.91084.com/tag.html 获取所有的顶级栏目列表带链接: {de ...

  2. Enhancer云开发平台发布了!打开浏览器写 SQL 做配置就能一站完成系统开发

    Enhancer 是专业的一站式信息系统开发云平台 绝大多数情况下,您只需编写 SQL,无需编写代码,即可快速完成各类系统的开发,并且获得可直接部署在您私有环境的应用程序.极大降低您的开发.运维.迭代 ...

  3. 熊猫多模式站群系统 开发日志 第二天

    高性能站群框架搭建完成. 接下来会将内置的两个站群模型整合进去 全局拦截器: 新添 客户端校验器 主要用于拦截一些非法的请求进入站群.比如机器人,爬虫之类的. 新添 域名权限校验器 主要用于拦截非法程 ...

  4. 谈谈秒杀系统的落地方案

    昨天的文章给秒杀系列开了一个头,今天会集中讲一下实现一个秒杀系统的思路和方案,不代表这就是最好的方案或者最佳实践,而是希望通过这篇文章,能起到抛砖引玉的作用,希望有更佳的思路提供出来. 秒杀系统要解决 ...

  5. springboot+veu实战项目-天猫整站

    目录 天猫整站 Springboot 一:技术准备 二:开发流程 三:本地演示 1 : 下载并运行 2 : 访问地址 3 : nginx 4 : nginx.conf 配置文件 5 : 启动nginx ...

  6. SpringBoot实战项目教程----springboot天猫整站

    目录 一:技术准备 二:开发流程 三:本地演示 1 : 下载并运行 2 : 访问地址 3 : nginx 4 : nginx.conf 配置文件 5 : 启动nginx 6 : 访问测试 7 : 动静 ...

  7. springboot实战项目----天猫整站---how2j

    目录 天猫整站 Springboot 一:技术准备 二:开发流程 三:本地演示 1 : 下载并运行 2 : 访问地址 3 : nginx 4 : nginx.conf 配置文件 5 : 启动nginx ...

  8. 商品配送服务核销水票水站桶装水系统开发

    商品配送服务核销水票水站桶装水系统开发 优势特点/ 专属门店 01 在线建立专属门店页面,首页可设置多张广告展示图,商品可支持多种 套餐同时销售,同时支持秒杀促销,有效促进新客户购买. 电子水票 02 ...

  9. cmsplus实战之仿[我扫网]之一:仿站开发工程简要说明及目的

    我扫网简介:我扫网,扫你所爱(wosao.cn),收录最新最全的微信公众账号,这里有你喜欢的明星微信.品牌微信,还有一大波微信美女等着你,是集微信公众账号导航.微信营销推广为一体的微信公众平台门户导航 ...

最新文章

  1. linux 常用的系统信息查看命令
  2. bestcoder #71 1003 找位运算的最大生成树
  3. 多线程中数据的并发访问与保护
  4. linux文件系统叫什么,【整理】什么是根文件系统(rootfs=Root Fils System)
  5. 小型项目服务器要多少,小型服务器需要什么配置
  6. php 判断接受邮件地址,PHP:电子邮件验证并接受来自特定域的电子邮件地址
  7. 使用goJenkins create job 返回500
  8. PowerDesigner的文章
  9. 1. 请简述mysql数据库的锁机制_【MySQL入门】之MySQL数据库的锁机制(二)
  10. PTA程序设计类实验辅助教学平台-基础编程题--JAVA--7.4 BCD解密
  11. win10下面安装MTK USB VCOM 驱动
  12. 计量论文stata代码大全
  13. 大学生计算机应用大赛广告设计,第11届全国大学生计算机应用能力与信息素养大赛 “平面视觉设计” 赛项圆满结束...
  14. 股票量化实盘交易接口如何做回测?
  15. 520评论点赞活动开奖
  16. mysql----where 1=1是什么意思
  17. Corner芯片TT,FF,SS
  18. 轻型载货汽车(离合器及传动轴设计)外文翻译
  19. 口碑预点单正餐先后付承接端哪些版本支持?
  20. 被这5个资源网站惊到了!老司机秒懂!

热门文章

  1. APP 转让问题记录-跳转微信小程序正在连接
  2. Android换肤之Android-skin-support
  3. 字典树简单实现 插入 查找 遍历
  4. 汽车车系 API数据接口
  5. 视线估计算法的工程实践
  6. python局部静态变量_python如何设置静态变量
  7. VMware虚拟机界面如何改为自动适应屏幕大小?图文详解
  8. IntelliJ IDEA 导入 setting
  9. 多平台购买门票,退款中的被消费,导致损失两张门票的屎蛋经历(追回钱的过程深刻展示了中国式踢皮球)
  10. 数学建模、运筹学之非线性规划