前言:
微信点餐项目实践——慕课网
项目介绍
前端是由Vue.js构建的WebApp,后端由Spring Boot打造,后端的前台页面使用Bootstrap+Freemarker+JQuery构建,后端和前端通过RESTful风格的接口相连。
数据库方面使用Spring Boot+JPA,兼顾Spring Boot+Mybatis;缓存方面,使用Spring Boot+Redis;基于Redis,应对分布式Session和锁;消息推送方面,使用WebSocket。
项目设计
角色划分
- 买家(手机端):由微信公众号提供的一个服务。
- 卖家(PC端):一个简单的商家管理系统
功能模块划分
- 功能分析
- 关系图
部署
买家端在手机端,卖家端在PC端,两端都会发出数据请求,请求首先到达nginx服务器,如果请求的是后端接口,nginx服务器会进行一个转发,转发到后面的Tomcat服务器,即我们的Java项目所在,如果这个接口作了缓存,那么就会访问redis服务器,如果没有缓存,就会访问我们的MySQL数据库。值得注意的是我们的应用是支持分布式部署的,也就是说图上的Tomcat表示的是多台服务器,多个应用。
数据库
共5个表,表之间的关系如下,其中商品表存放的就是商品的名称、价格、库存、图片链接等信息;类目表含有类目id、类目名字等信息,一个类目下有多种商品,类目表和商品表之间是一对多的关系;订单详情表含有购买的商品名称、数量、所属订单的订单号等信息;订单主表包含包含该订单的订单号、买家的信息、订单的支付状态等信息,订单主表和订单详情表之间是一对多的关系;最后是卖家信息表,存放的卖家的账号和密码等信息,作为卖家后台管理的权限认证。
项目使用的主要技术栈
- Spring Boot的相关特性
- Spring Boot+JPA
- Spring Boot+Redis
- Spring Boot+WebSocket
- 微信相关特征
- 微信支付、退款
- 微信授权登陆
- 微信模板消息推送
- 使用微信相关的开源SDK
- 利用Redis应用分布式Session和锁
- 对用户的登陆信息使用分布式Session存储
- 利用一个抢购商品的例子,来对Redis分布式锁进行详细的说明
开发环境及工具
- IDEA
- Maven
- Git
- MySQL
- Nginx
- Redis
- Postman模拟微信订单创建订单
- Fiddler对手机请求抓包
- Natapp内网穿透
- Apache ab模拟高并发,抢购一个商品
项目开发
买家端商品类目
买家端类目模块的开发,按照dao->service->api的顺序开发。
create类目表
1 | -- 类目 |
ProductCategory实体
1 | // 将数据库表单映射成对象加上注解 |
ProductCategoryDAO
1 | // 继承SpringBootJPA,泛型为实体对象和其对应主键类型 |
dao单元测试
1 | .class) (SpringRunner |
CategoryService
接口:
1 | public interface CategoryService { |
实现:
1 |
|
service单元测试
1 | .class) (SpringRunner |
买家端商品信息
create商品表
1 | -- 商品 |
ProductInfo实体
1 | // 将数据库表单映射成对象加上注解 |
ProductInfoDAO
1 | public interface ProductInfoDao extends JpaRepository<ProductInfo, String> { |
dao单元测试
1 | .class) (SpringRunner |
ProductService
1 | public interface ProductService { |
1 |
|
上面的代码会有一些目前还未涉及到的,这里先放上。同时这里涉及到编程中的一些类的包装和优化。
枚举类包装商品状态
1 | // 引入lombok.Getter自动生成get()方法 |
service单元测试
1 | .class) (SpringRunner |
BuyerProductController
商品列表api
1 | GET /sell/buyer/product/list |
参数
1 | 无 |
返回
1 | // 这里需要分析文档 |
通过对文档的分析,发现首先需要对数据进行封装
包装返回到前端的视图对象VO
ViewObject
1 | // 最外层 |
controller
1 | // 表示返回json格式 |
包装返回前端提示工具
1 | public class ResultVoUtil { |
买家端订单信息
create订单表
1 | -- 订单主表 |
OrderMaster实体
1 |
|
包装订单状态
1 |
|
OrderMasterDAO
1 | public interface OrderMasterDao extends JpaRepository<OrderMaster, String> { |
dao单元测试
1 | .class) (SpringRunner |
OrderService
包装数据传输对象(DTO)
1 |
|
定义异常
1 | public class SellException extends RuntimeException{ |
1 | // 异常信息结果枚举类 |
随机数生成主键
1 | public class GenKeyUtil { |
购物车DTO
1 |
|
数据类型转换
1 | public class OrderMaster2OrderDTOConverter { |
service
1 | public interface OrderService { |
1 |
|
BuyerOrderController
api
创建订单
1 | POST /sell/buyer/order/create |
参数
1 | name: "张三" |
返回
1 | { |
订单列表
1 | GET /sell/buyer/order/list |
参数
1 | openid: 18eu2jwk2kse3r42e2e |
返回
1 | { |
查询订单详情
1 | GET /sell/buyer/order/detail |
参数
1 | openid: 18eu2jwk2kse3r42e2e |
返回
1 | { |
取消订单
1 | POST /sell/buyer/order/cancel |
参数
1 | openid: 18eu2jwk2kse3r42e2e |
返回
1 | { |
表单验证
包装一个订单数据表单数据类
1 |
|
数据类型转换
1 | 4j |
controller
1 |
|
查询和取消订单安全做法
1 | public interface BuyerService { |
微信端开发
项目总结
RESTfulAPI
- REST的全称是Representational State Transfer,翻译下来就是“表现层状态转移”。
- REST的名称”表现层状态转化”中,省略了主语Resource。”表现层”其实指的是”资源”(Resources)的”表现层”。“资源”是REST架构或者说整个网络处理的核心。
- Server提供的RESTful API中,URL中只使用名词来指定资源,原则上不使用动词。
- 对资源的添加,修改,删除等操作,用HTTP协议里的动词来实现。如下:
GET 用来获取资源
POST 用来新建资源(也可以用于更新资源)
PUT 用来更新资源
DELETE 用来删除资源
- Server和Client之间传递某资源的一个表现形式,比如用JSON,XML传输文本。
- 用 HTTP Status Code传递Server的状态信息。比如最常用的 200 表示成功,500 表示Server内部错误等。
结合本项目做具体阐述:
Spring中也有Restful请求相关的注解@RestController
,它是Spring4.0新添加的,是@Controller
,@ResponseBody
的组合注解。该注解用于controller上,该注解下的方法返回的对象(可序列化的)可直接响应给客户端,以json格式展示,也可用于前后端分离的项目(返回的对象模型可以填充到前端界面中)。
1 |
|
ResultVO的定义如下:
1 |
|
对于上面商品列表的接口,http://127.0.0.1:8080/sell/buyer/order/list?openid=abcabcvc
。
首先,我们用浏览器访问,会返回如下的json数据:
使用前端界面访问,就会得到如下画面:
所以对于前后端分离的项目,前后端人员可以共同协商写一份API文档(包含访问接口,和返回数据格式),然后后端人员,就会根据根据接口进行开发,并按API文档要求返回指定格式的数据。
详情参考:
- 知乎热门话题: RESTful
- 阮一峰的两篇文章: 理解RESTful架构 RESTful API 设计指南
- Spring构建RESTful Web Service的GETTING STARTED教程: Building a RESTful Web Service
日志使用
什么是日志日志框架
- 一套能够实现日志输出的工具包。
- 能够描述系统运行状态的所有时间都可以算作日志,包括用户下线,接口超时,数据库崩溃。
日志框架的能力
- 定制输出目标。
- 定制输出格式。
- 日志携带的上下文信息,如时间戳、类路径、线程、调用对象等等。
- 运行时的选择性输出,比如现在的系统正常,我就只关心正常的日志,假如现在系统运行特别慢,那我可能就比较关心数据库访问层的问题,也就是DAO层的细节,这时候如果能够把这部分相关的日志打印出来,就很重要了。
- 灵活的配置。
- 优异的性能。
常用的日志框架
1 | 日志门面 日志实现 |
进过比较,我们选用SLF4j和Logback。Spring Boot里面用的就是SLF4j和Logback,在使用Spring Boot构建web项目是,我们引入了,下面的起步依赖:
1 | <dependency> |
这个起步依赖已经引入了SLF4j和Logback,所以我们不必再显示引入。
- 使用方法: 如果在A类中使用日志,先以该类为参数构造一个对象,如下:
1 | private final Logger logger=LoggerFactory.getLogger(A.class); |
然后就可以使用:
1 | log.debug("debug......"); |
更简洁的使用技巧
上面每次使用都要构造一个Logger对象,有没有很麻烦。
其实在IEDA里,我们可以有一点小技巧。可以不用构造Logger对象。
首先,添加以下依赖。
1 | <dependency> |
然后,在IDEA中安装lombok插件,然后在需要打印日志的类上用@Slf4j
注解。
这样在该类中就可以直接使用log
对象了。如下:
1 | .class) (SpringRunner |
- 在日志里输出变量,用占位符。
1 | String name="Mary"; |
Logback的配置
- 方法一:在application.yml中配置,这里能配置的东西比较简单,只能配置日志文件的路径,日志输出格式等一些简单的配置。
1 | logging: |
- 方法二:在logback-spring.xml配置,可以进行一些复杂的配置。比如有下面的需求:
- 区分info和error日志
- 每天产生一个日志文件
在resource
目录下建立logback-spring.xml文件。
1 |
|
对于每个配置项的含义,已用注解作简要说明。不过我们还是有必要了解一下日志级别,在package org.slf4j.event;
有一个Level的枚举类,它定义了日志级别,如下:
1 | public enum Level { |
里面定义的TRACE、DEBUG、INFO、WARN、ERROR,表明严重性逐渐增加。
lombok其他的作用
上面引入的lombok依赖还有其他的作用,我们在项目中与许多Entity和DTO,它们有许多字段,而且需要get、set方法。我们可以在该实体类上使用@Data注解,那么实体类就不需要显示写get、set方法了。
1 | import lombok.Data; |
如果该实体类只需get或set方法,那么就可以使用@Getter或@Setter注解。
Java8特性
项目中BuyerProductController.java中有这么一段代码。
1 | //1.查询所有的上架的商品 |
上面代码中涉及了一些java8的知识:
- Stream(流)
- lambda表达式
- 使用方法引用来简化lambda表达式
- 使用Stream操作集合
在学习lambda表达式之前,要先了解函数式接口。
函数式接口
函数式接口是只含有一个抽象方法的接口,比如下面就是一个函数式接口:
1 |
|
我们还可以使用@FunctionalInterface注解函数式接口,使用该注解后,该接口就只能定义一个抽象方法。
lambda表达式
我们可以使用lambda表达式来实现一个函数式接口,如下:
1 | public class MyTest { |
方法引用和构造器引用
lambda表达式还是比较常用,很简洁。不过还有比lambda表达式更简洁的写法,那就是方法引用,先上代码:
1 | //1.上面lambda可以用:引用类方法简化 |
以上是使用方法引用来简化lambda表达式。如果lambda表达式的方法体只有一个方法调用,可以使用方法引用来简化lambda表达式。
下面对几种方法引用的方式总结:
种类 | 使用方式 |
---|---|
引用类方法 | 类名::类方法 |
引用类的实例方法 | 类名::实例方法 |
引用特定对象的实例方法 | 特定对象::实例方法 |
引用构造器 | 类名::new |
下面一段代码用例子说明了上面四种情况:
1 |
|
使用Stream来操作集合
现在主要讲解流中和集合操作相关的的知识。
本文的开头productInfoList.stream()
返回一个Stream对象,下面挑选stream中一个典型的方法分析一下使用方式。
- 下面是map方法。
1 | /** |
上面注释就是说该方法返回一个流,该流是一个中间操作,它包含通过mapper函数运算后结果。
比如productInfoList.stream().map(e->e.getCategoryType())
就是将集合中商品的种类映射为一个Stream,它是一个中间流,再看后面一部分collect(Collectors.toList())
,它表示把这个中间流变成一个List。
下面我们对Collectors这个类进行探究。
Collectors
下面是Collectors类注释的截图:
使用方式说的很明白,详情参见:Collectors的API文档
下面用一个例子对上面的文档进行详细说明,建议在在IDEA里debug模式下运行,查看个变量的内容。
1 | public class MyTest1 { |