SOA思想
面向对象的编程思想OOP/ 面向接口开发/面向切面开发AOP/面向服务开发SOA
面向服务的架构(SOA)是一个组件模型,它将应用程序的不同功能单元(称为服务)进行拆分,并通过这些服务之间定义良好的接口和协议联系起来。接口是采用中立的方式进行定义的,它应该独立于实现服务的硬件平台、操作系统和编程语言。这使得构件在各种各样的系统中的服务可以以一种统一和通用的方式进行交互。
概括:SOA思想要求按照业务将服务进行拆分,之后按照同一的中立的接口进行交互.
RPC (Remote Procedure Call) 远程过程调用,简单的理解就是一个节点请求另一个节点的服务 --在服务之间,由第三方完成自己的任务的过程称之为远程过程调用.
微服务调用原理说明
标准:
1.根据业务拆分的思想 进行了分布式的设计
2.当服务发生异常时可以自动的实现故障的迁移 无需人为的干预.
传统服务调用方式
由于nginx做负载均衡时需要依赖配置文件.但是当服务器新增/减少时.都需要手动的修改.不能自动化的实现.所以暂时达不到微服务的标准.
微服务的调用原理介绍
步骤:
1.当服务的提供者启动时,会将自己的服务信息(服务名称/IP/端口号)进行记录,注册到注册中心中
2.注册中心服务器(zooKeeper)记录提供者的信息 维护服务列表
3.当服务消费者启动时会链接注册中心.
4.从注册中心中获取服务列表信息,方便下次调用 --貌似应该是zooKeeper注册中心下发的…
5.当消费者调用服务时,会根据负载均衡的机制挑选其中的一个服务提供者进行访问.
6.当服务提供者宕机时,由于注册中心有心跳检测机制,会维护服务列表.当宕机的提供者标识为down
7.当服务列表维护之后,会全网广播通知所有服务器的消费者更新服务列表信息. --eureka注册中心(强调可用性)不同于zooKeeper注册中心(强调一致性),eureka注册中心不会通知服务器的,需要服务器自己去获取!
关于集群说明–为什么集群是奇数台
原则: 搭建集群必须满足公式(最少宕机1台的情况??) 现有的节点数量 > N/2
为什么集群的最小单位是3台:
假设宕机1台:
1台 1-1=0 > 1/2 假的
2台 2-1=1 > 2/2 假的
3台 3-1=2 > 3/2 真的 3台是搭建集群的最小单位.
4台 4-1=3 > 4/2 真的 只要大于3台都可以搭建集群.
集群中最多允许宕机的台数为多少???
3台最多允许宕机几台? 最多宕机1台
4台最多允许宕机几台? 最多宕机1台
从实用性的角度考虑问题 发现搭建奇数台和偶数台的容灾能力相同.所以选用奇数台.
zk集群选举的原理
原理说明: zk集群的选举根据最大值优先的规则,进行选举. 如果集群一旦超过半数以上的票数同意,则当选主机,同时选举结束.
问题: 有1,2,3,4,5,6,7节点依次启动.
问题1: 谁当主机? 4当主机
问题2: 哪几台永远不能当选主机? 1 2 3 因为当4以上都宕机之后超过半数服务器宕机集群崩溃了…
====真正的Dubbo框架来了(大型项目,有实力的企业追求性能就用dubbo,后面微服务其他问题自己再集成)
Apache Dubbo |ˈdʌbəʊ| 是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。 –dubbo框架是序列化数据传输比SpringCloud的http传输快很多,性能更优!!有实力的大企业,可以使用dubbo框架后自己集成其他微服务的功能单元 -----dubbo框架是Web端controller直接调用远程端的service,而SpringCloud是Web服务器端controller调用自己服务器feign接口(类似于service…),feign接口再去http协议通过注册中心远程调用服务端的controller,controller再调用service执行业务!!!
–注意feign接口里的@GetMapping,@PostMapping等只是用了spring的注解一样的名字,解析时feign有自己解析注解的代码,这里是远程调用而不是spring的请求接收…
Dubbo工作原理
Dubbo框架介绍
Dubbo框架通讯的方式
说明:只要使用Dubbo框架 ,在内部使用了dubbo协议进行通讯,其中的IP地址是动态生成的.并且端口号是访问服务的唯一标识信息.
ZK存储数据的结构
说明:zk中的数据的存储的方式是树形结构的.一般三级. ls --一直 ls 即可一级一级打开…
关于负载均衡的说明
1 集中式的负载均衡
说明:由于nginx处于负载均衡的中心,所以什么样的服务都会经过nginx之后转向到不同的服务器中. 所以会造成nginx的负载压力很大.
nginx的主要的作用是反向代理.
2 客户端的负载均衡
说明:在微服务调用过程中每个服务的消费者都可以在客户端实现负载均衡的操作,在每次请求之前通过服务列表获取将要访问的服务信息.实现了压力私有化.
Dubbo负载均衡的方式
名称都是类名的前半部分都小写即可.
1.RandomLoadBalance 随机负载均衡 语法: random 默认的
2.RoundRobinLoadBalance 轮询策略 语法: roundrobin
3.ConsistentHashLoadBalance 一致性hash算法 将消费者与服务提供者绑定 语法: consistenthash
4.LeastActiveLoadBalance 挑选负载压力小的服务器进行访问 语法: leastactive
Dubbo入门案例
导入项目方式一:将项目复制到idea的project工作空间中后下面的操作
导入方式二:
将项目复制到工作空间后idea中自动显示了该项目,直接最右边Maven中点+加号将新添加的项目的pom.xml文件选中加入即可!!!
说明:dubbo-jt模块里创建
dubbo-jt-consumer 消费者
dubbo-jt-interface 中间中立连接接口
dubbo-jt-provider1 消费提供者1
dubbo-jt-provider2 消费提供者2
消费者调用中立接口,而消费提供者实现了中立接口.所以消费者在调用提供者来实现业务功能时仿佛调用自己项目里的service…
==示例代码 –这里只是模拟dubbo框架的远程调用服务…
- 提供者的说明 – 编辑Dubbo实现类ServiceImple
package com.jt.dubbo.service;
import com.alibaba.dubbo.config.annotation.Service; //这里的导包留着是让你们看清楚--这是dubbo框架下的service包!!!!!!
import com.jt.dubbo.mapper.UserMapper;
import com.jt.dubbo.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
@Service(timeout=3000) //3秒超时 内部实现了rpc //dubbo框架下的包
//@org.springframework.stereotype.Service//将对象交给spring容器管理
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public List<User> findAll() {
System.out.println("我是第一个服务的提供者");
return userMapper.selectList(null);
}
@Override
public void saveUser(User user) {
userMapper.insert(user);
}
}
- YML配置文件说明
server:
port: 9000 #定义端口
spring:
datasource:
#引入druid数据源
#type: com.alibaba.druid.pool.DruidDataSource
#driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/jtdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
username: root
password: root
#关于Dubbo配置
dubbo:
scan:
basePackages: com.jt #指定dubbo的包路径
application: #应用名称
name: provider-user #一个接口对应一个服务名称
registry:
address: zookeeper://192.168.126.129:2181?backup=192.168.126.129:2182,192.168.126.129:2183 #注册中心zk一般第一个写从机bacaup=后面随意..#zk集群 主机中的信息与从机中的信息一致的 从zk中获取数据时连接的从机,主机的作用就是监控集群的运行状态,负责数据的同步
protocol: #指定协议
name: dubbo #使用dubbo协议(tcp-ip) web-controller直接调用sso-Service
port: 20880 #每一个服务都有自己特定的端口 不能重复.
#MP的配置
mybatis-plus:
type-aliases-package: com.jt.dubbo.pojo #配置别名包路径
mapper-locations: classpath:/mybatis/mappers/*.xml #添加mapper映射文件
configuration:
map-underscore-to-camel-case: true #开启驼峰映射规则
- 服务消费者说明–Controller层
package com.jt.dubbo.controller;
import com.alibaba.dubbo.config.annotation.Reference; //@Reference注解看清楚是dubbo框架下的!!!
import com.jt.dubbo.pojo.User;
import com.jt.dubbo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class UserController {
利用dubbo的方式为接口创建代理对象 利用rpc调用
//check=false 先启动提供者/消费者没有关系 --貌似不太好用可能会报错,直接每次都需要先启动服务提供者吧...
@Reference(check = false,timeout = 3000) //(loadbalance="leastactive")
private UserService userService; //直接调用dubbo框架下的中立接口..
/**
* Dubbo框架调用特点:远程RPC调用就像调用自己本地服务一样简单
*/
@RequestMapping("/findAll")
public List<User> findAll(){
//远程调用时传递的对象数据必须序列化.
return userService.findAll();
}
@RequestMapping("/saveUser/{name}/{age}/{sex}") //当传递多个参数时,若参数名称与对象的属性名称一致,则可以直接用对象接收参数
public String saveUser(User user) {
userService.saveUser(user);
return "用户入库成功!!!";
}
}
启动提供者,启动消费者.完成项目简单测试…
问题1: 如果将其中的一个服务的提供者关闭,问 用户访问是否受影响?? 不受任何影响
问题2: 如果将dubbo中的注册中心全部关闭,问用户访问是否受到影响??? 不受影响,因为消费者将服务列表数据保存到本地.
京淘项目Dubbo改造
1.jt-common充当接口项目
2.jt-sso 充当用户的提供者 服务端口号20880 服务名称 provider-sso 接口DubboUserService
3.jt-web 充当用户的消费者
=开始代码部分=
1 jt父级工程的pom.xml文件中添加Dobbu框架的jar包
<!--引入dubbo配置 -->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>0.2.0</version>
</dependency>
2 编辑Dubbo接口
说明:在jt-common中添加Dubbo的业务接口
3 编辑jt-sso的服务提供者 --service实现类
4 编辑YML配置文件
server:
port: 8093
servlet:
context-path: /
spring:
datasource:
#引入druid数据源
#type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/jtdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
username: root
password: root
#提供了MVC的支持
mvc:
view:
prefix: /WEB-INF/views/
suffix: .jsp
#mybatis-plush配置
mybatis-plus:
type-aliases-package: com.jt.pojo
mapper-locations: classpath:/mybatis/mappers/*.xml
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.jt.mapper: debug
#关于Dubbo配置
dubbo:
scan:
basePackages: com.jt #指定dubbo的包路径
application: #应用名称
name: provider-sso #一个接口对应一个服务名称
registry: #zk集群 主机中的信息与从机中的信息一致的 从zk中获取数据的时候链接的从机 主机的作用就是监控集群
address: zookeeper://192.168.126.129:2181?backup=192.168.126.129:2182,192.168.126.129:2183
protocol: #指定协议
name: dubbo #使用dubbo协议(tcp-ip) web-controller直接调用sso-Service
port: 20880 #每一个服务都有自己特定的端口 不能重复.
5 编辑服务消费者
UserController–注入UserService注解
6 编辑YML配置文件
server:
port: 8092
spring: #定义springmvc视图解析器
mvc:
view:
prefix: /WEB-INF/views/
suffix: .jsp
dubbo:
scan:
basePackages: com.jt
application:
name: consumer-web #定义消费者名称
registry: #注册中心地址
address: zookeeper://192.168.126.129:2181?backup=192.168.126.129:2182,192.168.126.129:2183
用户模块实现 --用户注册
1 页面分析
2 页面JS分析 --根据页面url地址 查找页面JS的位置
3 编辑UserController
/**
* 完成用户注册操作.
* url地址: http://www.jt.com/user/doRegister
* 参数: {password:_password,username:_username,phone:_phone}
* 返回值: SysResult对象 返回的是JSON串
* 业务说明:通过dubbo框架将user信息RPC传入jt-sso实现数据的入库操作.
* */
@RequestMapping("/doRegister")
@ResponseBody
public SysResult saveUser(User user){
dubboUserService.saveUser(user);
return SysResult.success();
}
4 编辑UserService
package com.jt.service;
@Service //dubbo的注解
public class DubboUserServiceImpl implements DubboUserService{
@Autowired
private UserMapper userMapper;
/**
* 1.邮箱暂时使用电话号码代替
* 2.需要将密码进行加密处理 md5/md5-hash
* @param user
* @return
*/
@Override
@Transactional
public void saveUser(User user) {
//1.获取明文
String password = user.getPassword();
//2.利用Spring的工具API进行加密操作
password = DigestUtils.md5DigestAsHex(password.getBytes());
user.setPassword(password).setEmail(user.getPhone());
userMapper.insert(user);
}
}
关于POJO转化异常说明
报错说明: 由于SpringBoot配置了热部署的工具,当代码进行修改之后,程序就会重新启动. 在重启的过程中程序又会再次链接zookeeper注册中心.由于zk的心跳检测机制存在超时时间,可能在zk中会出现2条一模一样的服务的提供者的信息.
解决方案: 需要手动的重启服务器即可.
=单点登录的项目正式代码=
需求:用户只需要登录一次,那么就可以访问其他的认证系统,无需用户再次登录.
单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是目前比较流行的 一种登录方式
单点登录实现策略
步骤:
1.当用户输入用户名和密码时需要将数据传递给jt-web服务器进行登录操作.
2.jt-web服务器需要将数据传到jt-sso服务器中进行数据的校验.
3.jt-sso根据username/password查询数据库校验数据是否有效.
4.如果用户名和密码正确则将数据经过处理之后保存到redis中 KEY=UUID(每次生成的都不一样) VALUE=“userJSON”
5.如果用户写入redis成功,之后需要将用户的登录的凭证返回给客户端.
6.JT-WEB服务器将获取的TICKET信息保存到客户端的Cookie中,方便下次使用. 并且要求cookie共享的.
用户登录具体实现
1 用户登录页面分析
2 页面JS分析
$.ajax({
type: "POST",
url: "/user/doLogin?r=" + Math.random(),
contentType: "application/x-www-form-urlencoded; charset=utf-8",
data: {username:_username,password:_password},
dataType : "json",
error: function () {
$("#nloginpwd").attr({ "class": "text highlight2" });
$("#loginpwd_error").html("网络超时,请稍后再试").show().attr({ "class": "error" });
$("#loginsubmit").removeAttr("disabled");
$this.removeAttr("disabled");
},
success: function (result) {
//如果数据不为null时执行
if (result) {
var obj = eval(result);
if (obj.status == 200) {
obj.success = "http://www.jt.com";
.....
3 编辑UserController
/**
* 完成用户的登录操作
* url地址:http://www.jt.com/user/doLogin?r=0.8989367429030823
* 参数: username/password
* 返回值: SysResult对象 的JSON的数据.
* cookie.setMaxAge(-1); 关闭浏览器会话时删除
* cookie.setMaxAge(0); 立即删除cookie
* cookie.setMaxAge(100); cookie可以存储的时间单位是秒
*
* http://www.jt.com/saveUser/xxx
* cookie.setPath("/");
* cookie.setPath("/add");
*/
@RequestMapping("/doLogin")
@ResponseBody
public SysResult doLogin(User user, HttpServletResponse response){
//1.实现用户的登录操作!!!
String ticket = dubboUserService.doLogin(user);
//2.校验ticket是否有值.
if(StringUtils.isEmpty(ticket)){
//用户名或者密码错误
return SysResult.fail();
}
//3.如果用户的ticket不为null,则表示登录正确,需要将数据保存到cookie中
//Cookie要求 1.7天有效 2.要求cookie可以在jt.com的域名中共享 3.cookie权限 /
Cookie cookie = new Cookie("JT_TICKET",ticket);
cookie.setMaxAge(7*24*3600);
cookie.setDomain("jt.com"); //在jt.com中实现页面共享.
cookie.setPath("/"); //定于cookie的权限根目录有效
response.addCookie(cookie); //利用response将cookie保存到客户端中.
return SysResult.success();
}
4 编辑UserServiceImpl – dubbo框架的远程调用
—web服务器的controller远程调用sso单点登录的service
/**
* 1.根据用户名和密码查询数据库
* 2.校验用户数据的有效性.
* 3.如果用户的数据是正确的 则开始进行单点登录操作.
* 4.如果用户数据不正确 则ticket数据为null即可.
* @param user
* @return
*/
@Override
public String doLogin(User user) {
//1.将密码进行加密处理
String password = DigestUtils.md5DigestAsHex(user.getPassword().getBytes());
user.setPassword(password);
//如果传递的是对象,则根据对象中不为null的属性充当where条件
QueryWrapper<User> queryWrapper = new QueryWrapper<>(user);
User userDB = userMapper.selectOne(queryWrapper);
//2.校验数据是否有效
if(userDB == null){
return null;
}
//userDB数据不为null,用户的输入信息正确.开启单点登录操作.
//3.1动态生成uuid
String ticket = UUID.randomUUID().toString().replace("-", "");
//3.2脱敏处理
userDB.setPassword("123456你信不??");
String userJSON = ObjectMapperUtil.toJSON(userDB);
//3.3 将数据保存到redis中
jedisCluster.setex(ticket, 7*24*60*60, userJSON);
return ticket;
}
5 数据校验
小结一下:
用户浏览器输入用户名和密码之后登录提交到
web的controller中远程调用sso的service查询数据库该用户名和密码是否与数据库一致(是否存在)
若不存在则在Web的controller中直接返回return SysResult.fail();***若存在将sso的service返回的ticket值作为value,再另指定一个名字如"JT_TICKET"作为key存入浏览器cookie中!!***–将该cookie设置为 有效时间7天,在指定域名jt.com的域名中共享,指定/根目录有效
Cookie cookie = new Cookie("JT_TICKET",ticket);
cookie.setMaxAge(7*24*3600);
cookie.setDomain("jt.com"); //在jt.com中实现页面共享.
cookie.setPath("/"); //定于cookie的权限根目录有效
response.addCookie(cookie); //利用response将cookie保存到客户端中.
return SysResult.success();
在service中判断是否有对应的数据时!!!
若存在则先UUID随机生成一个唯一的字符串作为ticket --用来作为redis缓存数据的key值,再将user的密码脱敏处理后将User对象转换为Json格式作为value存入redis缓存中!!!
貌似浏览器每次访问都会携带存在的cookie数据!!
==========
用户登录 登录用户名 回显
分析:
当用户登录成功之后会生成COOKIE信息.根据cookie获取TICKET信息,之后根据ticket获取user信息,.之后在首页展现用户名称即可.
页面JS分析
1 编辑JT-SSO UserController
/**
* 根据ticket信息查询用户的json信息 jsonp请求 返回值使用特定的对象封装.
* url地址:http://sso.jt.com/user/query/ca620491866a42e596b29ee52fc27aff?callback=jsonp1600333068471&_=1600333068521 --js中提交url请求时将ticket加入到url中了
*/
@RequestMapping("/query/{ticket}")
public JSONPObject findUserByTicket(@PathVariable String ticket,
HttpServletResponse response,
String callback){
if(jedisCluster.exists(ticket)){
//若缓存中存在该ticket(key)则可以正确返回给js页面的ajax请求,返回值类型为jsonp格式用JSONPObject对象封装(callback,SysResult类型的封装了 用户信息的json对象--因为redisCluster中存储对象都必须先转化为Json格式..所以前面存入redis中的对象就是转化为json格式的用户信息对象 --所以这里从缓存中直接取出来的是json格式的用户对象.. )
String userJSON = jedisCluster.get(ticket);
return new JSONPObject(callback, SysResult.success(userJSON));//这里返回值类型为SysResult(就是JsonResult)--封装了状态信息,success(data)方法中data数据类型任意.
}else{
//如果根据ticket查询有误,则应该删除Cookie信息.
Cookie cookie = new Cookie("JT_TICKET","");
cookie.setDomain("jt.com");
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
return new JSONPObject(callback, SysResult.fail());
}
}
========
用户模块–用户登出
1 编辑UserController
/**
* 实现用户的登出操作 要求删除cookie 和redis中的数据(key)
* 步骤: 通过cookie获取ticket信息.
* url: http://www.jt.com/user/logout.html
* 参数: 暂时没有
* 返回值: 重定向到系统首页
*/
@RequestMapping("/logout")
public String logout(HttpServletRequest request,HttpServletResponse response){
Cookie[] cookies = request.getCookies();
if(cookies !=null && cookies.length >0){
for(Cookie cookie : cookies){
if("JT_TICKET".equals(cookie.getName())){
//获取value之后删除cookie
String ticket = cookie.getValue(); //根据cookie中的(value)存储的ticket删除redis缓存中的数据.
jedisCluster.del(ticket); //删除redis中的数据
//删除cookie时 必须与原来的cookie数据保持一致
cookie.setDomain("jt.com");
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
break;
}
}
}
return "redirect:/";
}
Cookie优化–封装为工具API===
package com.jt.util;
public class CookieUtil {
/**
* 该工具API主要的任务
* 1.根据cookie的名称 返回cookie对象
* 2.根据cookie的名称 返回valve的值
* 3.新增cookie方法
* 4.删除cookie方法
*/
public static Cookie getCookie(String cookieName, HttpServletRequest request){
Cookie[] cookies = request.getCookies();
if(cookies !=null && cookies.length >0) {
for (Cookie cookie : cookies) {
if (cookieName.equals(cookie.getName())) {
return cookie;
}
}
}
return null ;
}
public static String getCookieValue(String cookieName,HttpServletRequest request){
Cookie cookie = getCookie(cookieName, request);
return cookie ==null?null:cookie.getValue();
}
public static void addCookie(String cookieName, String cookieValue, String path,
String domain, int maxAge,
HttpServletResponse response){
Cookie cookie = new Cookie(cookieName,cookieValue);
cookie.setPath(path);
cookie.setDomain(domain);
cookie.setMaxAge(maxAge);
response.addCookie(cookie);
}
public static void deleteCookie(String cookieName,String path,
String domain,HttpServletResponse response){
addCookie(cookieName,"",path, domain, 0, response);
}
}
======
重构用户登出操作
/**
* 实现用户的登出操作 要求删除cookie 和redis中的数据(key)
* 步骤: 通过cookie获取ticket信息.
* url: http://www.jt.com/user/logout.html
* 参数: 暂时没有
* 返回值: 重定向到系统首页
*/
@RequestMapping("/logout")
public String logout(HttpServletRequest request,HttpServletResponse response){
String ticket = CookieUtil.getCookieValue("JT_TICKET", request);
if(!StringUtils.isEmpty(ticket)){
//1.删除redis
jedisCluster.del(ticket);
//2.删除cookie
CookieUtil.deleteCookie("JT_TICKET", "/", "jt.com", response);
}
return "redirect:/";
}
===============
购物车模块实现
1 创建jt-cart项目
2 添加继承/依赖/插件
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>jt-cart</artifactId>
<parent>
<artifactId>jt</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<!--添加依赖项-->
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>jt-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<!--添加插件-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3 编辑POJO
@TableName("tb_cart")
@Data
@Accessors(chain = true)
public class Cart extends BasePojo{
@TableId(type = IdType.AUTO)
private Long id; //主键自增
private Long userId; //用户id
private Long itemId; //商品id
private String itemTitle;
private String itemImage;
private Long itemPrice;
private Integer num;
}
编辑Cart代码结构
4 编辑YML配置文件
server:
port: 8094
servlet:
context-path: /
spring:
datasource:
#引入druid数据源
#type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/jtdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
username: root
password: root
#提供了MVC的支持
mvc:
view:
prefix: /WEB-INF/views/
suffix: .jsp
#mybatis-plush配置
mybatis-plus:
type-aliases-package: com.jt.pojo
mapper-locations: classpath:/mybatis/mappers/*.xml
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.jt.mapper: debug
#关于Dubbo配置
dubbo:
scan:
basePackages: com.jt #指定dubbo的包路径
application: #应用名称
name: provider-cart #一个接口对应一个服务名称
registry: #zk集群 主机中的信息与从机中的信息一致的 从zk中获取数据的时候链接的从机 主机的作用就是监控集群
address: zookeeper://192.168.126.129:2181?backup=192.168.126.129:2182,192.168.126.129:2183
protocol: #指定协议
name: dubbo #使用dubbo协议(tcp-ip) web-controller直接调用sso-Service
port: 20881 #每一个服务都有自己特定的端口 不能重复.
貌似:
//提供者是微服务,提供服务的 provider 供应者
//生产者是业务层,产生数据传递数据的!!!就是说微服务提供服务的都是提供者
producer制作人,生产者
先这样记着…后面可能有点混用…
=================
购物车列表展现
分析:
当用户点击购物车按钮时,应该根据userId查询购物车数据信息,之后在cart.jsp中展现列表信息.
页面js分析
1 编辑CartController --Web端的controller
@Controller
@RequestMapping("/cart")
public class CartController {
@Reference(check = false,timeout = 3000) //dubbo框架的
private DubboCartService cartService;
/**
* 1.购物车列表数据展现
* url地址: http://www.jt.com/cart/show.html
* 参数: 暂时没有
* 返回值: 页面逻辑名称 cart.jsp
* 页面取值: ${cartList}
* 应该将数据添加到域对象中 Request域 model工具API操作request对象
*/
@RequestMapping("/show")
public String show(Model model){ //
//1.暂时将userId写死 7L
Long userId = 7L;
List<Cart> cartList = cartService.findCartListByUserId(userId);
model.addAttribute("cartList",cartList);
return "cart"; //因为要跳转页面 并且前端中用EL表达式取值--页面取值: ${cartList}所以前面数据要存入域中!!!四大域:pageContext域—(PageContext)/request域(HttpServletRequest) /session 域 (HttpSession)/application(ServletContext)
}
}
2 编辑CartService --服务器端的Service (dubbo框架的远程调用) --根据userId查询购物车数据信息
package com.jt.service;
@Service
public class DubboCartServiceImpl implements DubboCartService{
@Autowired
private CartMapper cartMapper;
@Override
public List<Cart> findCartListByUserId(Long userId) {
QueryWrapper<Cart> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", userId);
return cartMapper.selectList(queryWrapper);
}
}
=================
完成购物车数量修改操作
分析:
url地址: http://www.jt.com/cart/update/num/562379/9
页面JS分析
$(".increment").click(function(){
var _thisInput = $(this).siblings("input");
_thisInput.val(eval(_thisInput.val()) + 1); //eval将数据都转换为数字..有用没有的加上了.. 点击加号就加1.. $.post("/cart/update/num/"+_thisInput.attr("itemId")+"/"+_thisInput.val(),function(data){
TTCart.refreshTotalPrice();
});
});
1 编辑CartController
/**
* 业务说明: 完成购物车数量的更新操作
* url地址: http://www.jt.com/cart/update/num/562379/9 //分析js得出最后两个数字为/{itemId}/{num}
* 参数: 暂时没有
* 返回值: void
*/
@RequestMapping("/update/num/{itemId}/{num}")
@ResponseBody //ajax结束的标识符. 以后记住了!!!所有的ajax请求有返回值的就Json返回没有返回值的也必须加@ResponseBody该注解虽然无返回值,但是返回个ajax的类型依然必须是json..--可以将该注解看成是ajax请求的结束标识符!!!!!!!!!!!!
public void updateCartNum(Cart cart){ //如果{name} 与属性的名称一致,则可以自动的赋值.
Long userId = 7L;
cart.setUserId(userId);
cartService.updateCartNum(cart);
}
2 编辑CartService
/**
* 更新购物车的数量 num
* @param cart
* Sql: update tb_cart set num = #{num},updated = now()
* where user_id = #{user} and item_id = #{itemId}
*/
@Override
public void updateCartNum(Cart cart) { //itemId,userId
Cart cartTemp = new Cart();
cartTemp.setNum(cart.getNum());
UpdateWrapper<Cart> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("user_id", cart.getUserId())
.eq("item_id", cart.getItemId());
cartMapper.update(cartTemp,updateWrapper);
}
=============
购物车删除操作
分析:
说明: 根据itemId和userId删除购物车数据.重定向到购物车列表页面即可.
1 编辑CartController --Web服务器端controller通过dubbo框架远程调用
/**
* 删除购物车数据
* url地址:http://www.jt.com/cart/delete/562379.html --设置了后缀配置类拦截html结尾的请求(这里只是一个伪静态)
* 参数: 562379
* 返回值: 购物车列表页面
*/
@RequestMapping("/delete/{itemId}")
public String deleteCart(Cart cart){
Long userId = 7L;
cart.setUserId(userId);
cartService.deleteCart(cart);
return “redirect:/cart/show.html”;
}
2 编辑CartService – service服务器端service
@Override
public void deleteCart(Cart cart) { //itemId/userId
//根据对象中不为null的元素充当where条件
cartMapper.delete(new QueryWrapper<>(cart)); //条件构造器中
}
===========
购物车新增操作
分析:
如果用户点击购入购物车时,应该将用户的提交数据插入到tb_cart表中,并且重定向到购物车列表页面.
注意事项: 如果用户重复提交数据,则应该数量更新.
页面js分析
<form id="cartForm" method="post">
<input class="text" id="buy-num" name="num" value="1" onkeyup="setAmount.modify('#buy-num');"/>
<input type="hidden" class="text" name="itemTitle" value="${item.title }"/>
<input type="hidden" class="text" name="itemImage" value="${item.images[0]}"/>
<input type="hidden" class="text" name="itemPrice" value="${item.price}"/>
</form>
1 编辑CartController --web服务器
/**
* 业务需求: 完成购物车入库操作
* url地址: http://www.jt.com/cart/add/562379.html
* 参数: form表单提交的数据/itemId
* 返回值: 重定向到购物车列表页面
*/
@RequestMapping("/add/{itemId}")
public String addCart(Cart cart){
Long userId = 7L;
cart.setUserId(userId);
cartService.addCart(cart);
return "redirect:/cart/show.html";
}
2 编辑CartService --service服务器
/**
* 思路:
* 1.先查询数据库 user_id/item_id
* 2.判断返回值是否有结果
* true: 只做数量的更新
* false: 直接入库即可
*/
@Override
public void addCart(Cart cart) {
QueryWrapper<Cart> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", cart.getUserId());
queryWrapper.eq("item_id", cart.getItemId());
Cart cartDB = cartMapper.selectOne(queryWrapper);
if(cartDB == null){
//新增入库
cartMapper.insert(cart);
}else{
//更新数量 MP方式
int num = cart.getNum() + cartDB.getNum(); //数量求和
cartDB.setNum(num).setUpdated(new Date());
cartMapper.updateCartNum(cartDB);
}
}
3 编辑CartMapper --直接用MP有点麻烦不如直接手写sql语句
public interface CartMapper extends BaseMapper<Cart> {
@Update("update tb_cart set num=#{num},updated=#{updated} where user_id=#{userId} and item_id =#{itemId}")
void updateCartNum(Cart cartDB);
}
==将jt-manage改造为Dubbo项目
说明:前面manage模块功能已实现,这里仅作项目上的部分(微服务)优化示例
1 创建DubboItemServiceImpl实现dubbo框架下的第三方公共接口(在common通用模块中自己编写的接口)
说明这里dubbo框架的应用简述:
- 将common通用模块定义为了第三方的公共接口
- 其他项目都添加了该模块为依赖(所以这里可以用它做dubbo框架的第三方借口了…)
- Web服务器端controller中添加了dubbo框架的注解@Reference就可以直接调用第三方接口了
- 远程的Service服务器端将dubbo配置添加到yml中,添加了dubbo框架的@Service注解并且实现了第三方接口(通用模块)
- 所以:这样controller就可以远程调用service了
商品详情展现
分析:
当用户点击某个商品时,需要跳转到商品的详情页面中.根据itemId展现商品信息.并且在item.jsp中展现数据.
页面js分析
1 编辑ItemController
@Controller
public class ItemController {
@Reference(check = false,timeout = 3000) //超时时间一般必须设置,否则客户端半天连接不上体验会很差
private DubboItemService itemService;
/**
* 业务: 根据itemId查询商品信息(item/itemDesc)
* url地址: http://www.jt.com/items/562379.html
* 参数: 注意restFul风格即可
* 返回值: 页面逻辑名称 item.jsp
* 页面取值: ${item.title }/${itemDesc.itemDesc}
* */
@RequestMapping("/items/{itemId}")
public String findItemById(@PathVariable Long itemId, Model model){
Item item = itemService.findItemById(itemId);
ItemDesc itemDesc = itemService.findItemDescById(itemId);
model.addAttribute("item", item);
model.addAttribute("itemDesc", itemDesc);
return "item";
}
}
2 编辑DubboItemServiceImpl
package com.jt.web.service;
@Service //dubbo的注解
public class DubboItemServiceImpl implements DubboItemService {
@Autowired
private ItemMapper itemMapper; //商品信息
@Autowired
private ItemDescMapper itemDescMapper; //商品详情信息.
@Override
public Item findItemById(Long itemId) {
return itemMapper.selectById(itemId);
}
@Override
public ItemDesc findItemDescById(Long itemId) {
return itemDescMapper.selectById(itemId);
}
}
3 service端服务器的编辑YML配置文件
#添加dubbo的配置
dubbo:
scan:
basePackages: com.jt #指定dubbo的包路径
application: #应用名称
name: provider-manage #一个接口对应一个服务名称
registry: #zk集群 主机中的信息与从集中的信息一致的 从zk中获取数据时连接的从机,主机的作用就是监控集群的运行状态,负责数据的同步
address: zookeeper://192.168.126.129:2181?backup=192.168.126.129:2182,192.168.126.129:2183
protocol: #指定协议
name: dubbo #使用dubbo协议(tcp-ip) web-controller直接调用sso-Service
port: 20882 #每一个服务都有自己特定的端口 不能重复.
=项目权限控制–拦截器HandlerInteceptor=
没有登录的条件下不允许访问购物车/订单等敏感业务.跳转到登录页面–拦截器实现 HandlerInterceptor–利用拦截器的机制 要求动态的获取userId,之后将参数传递给Controller中
思路:cookie—>ticket---->userJSON---->userId
知识拾遗:
- AOP: 对原有的方法进行扩展.在原有的基础之上提供了额外的操作. 业务控制的.Service层
- 拦截器: 控制了程序的执行轨迹.满足条件时才会执行任务. 控制的request对象/response对象 控制用户的请求.
- 所以综上:貌似AOP和拦截器不能混为一谈,各有各的功能
- 数据传输. request对象 /ThreadLocal --一个请求就会开启一个线程,同一个线程内才可以使用ThreadLocal本地线程这个对象传输数据(之前不用他们传数据是因为之前的各层级类对象都交给了spring框架的容器管理,哪个类层级中需要别的层的类直接注入对象后用该对象直接调用目标层级的类的方法实现其功能,获取需要的值即可 --现在是要另一种传输数据的方式例如直接将数据存入ThreadLocal线程中,在该线程执行中的任意位置都可以利用该线程对象获取需要的数据了!!不需要对象注入等…)
==
5 SpringMVC程序调用流程
说明:
1 url请求发来到达DispatcherServlet(前端控制器只做调度…),DispatcherServlet将请求发送到HandlerMapping(处理器映射器)***
(里面存的是k-v结构,貌似k就是url路径v就是与该路径对应的controller里的具体的目标方法),HandlerMapping处理后将url对应的v值Controller的名字返回给DispatcherServlet(貌似应该携带者目标方法的名字…目标方法上有@RequestMapping("/xx"))
2 DispatcherServlet将Controller的名字发送给HandlerAdaptor(处理器适配器),HandlerAdaptor根据该名字适配到对应的controller(整个调用过程都可以称之为后端Handler处理器*)中,controller调service,service调dao,dao连接数据库获取数据之后一级级返回数据,后端处理器Handller将数据封装为ModelAndView对象传给HandlerAdaptor,HandlerAdaptor 将ModelAndView返回给DispatcherServlet(ModelAndView对象中封装有model对象data数据和view对象的页面逻辑名称如index/addUser等html或jsp的页面名称)
3 DispatcherServlet将ModelAndView对象发送给ViewReslover(试图解析器) ViewReslover将view对象加上前缀后缀后形成完整的url路径解析成最终的View对象返回个DispatcherServlet
4 DispatcherServlet再将Model对象发动给上一步ViewReslover解析成的最终的View对象做视图渲染(貌似就是将Model中的data数据渲染到页面上(View对象上))
整个SpringMVC的执行流程完成!!
==
6 拦截器工作原理
说明:拦截器有好多个,pre前置拦截器(previous??貌似是 在…之前),业务完成之后的post拦截器,视图渲染完成后释放资源的after拦截器等等
拦截器配置
@Configuration //告诉spring这是一个配置类//配置web.xml配置文件
public class MvcConfigurer implements WebMvcConfigurer{
//开启匹配后缀型配置,为了将来程序实现静态页面的跳转而准备的 --可以配置拦截html页面,这里没具体配置貌似可以拦截所有的页面(比如乱写的后缀.daffadd都可以拦截到并最终跳转到对应的jsp页面上)..
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch(true);
}
//添加拦截器配置
@Autowired
private UserInterceptor userInterceptor; //自己写的拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userInterceptor).addPathPatterns("/cart/**","/order/**");//为自己写的拦截器指定拦截哪些访问路径的请求
}
}
拦截器业务实现
package com.jt.interceptor;
//处理器拦截器 一般在接口中定义default的方法可以使得实现类不必重写所有的接口的方法.(需要哪个重写哪个即可)
@Component //将拦截器交给Spring容器管理
public class UserInterceptor implements HandlerInterceptor { //自己定义的拦截器必须实现spring框架的HandlerInterceptor这个公共的接口
@Autowired
private JedisCluster jedisCluster;
/**
* 返回值说明:
* boolean: true 放行
* : false 拦截 一般配合重定向的方式使用
* 业务说明:
* 如果用户已经登录则程序放行.否则拦截
* 判断依据: 1.是否有cookie 2.redis中是否有记录.
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.动态获取JT_TICKET数据
String ticket = CookieUtil.getCookieValue("JT_TICKET",request);
if(!StringUtils.isEmpty(ticket)){
//2.判断redis中是否有数据
if(jedisCluster.exists(ticket)){
//4.动态获取user信息.将用户信息交给request对象传递给下一级
String userJSON = jedisCluster.get(ticket); //获取到user对象的json类型的 --因为redis中不能直接存user对象..
User user = ObjectMapperUtil.toObject(userJSON,User.class); //利用工具类API将json类型的对象转换为user对象
//当前的对象会携带当前的用户信息!!!!
request.setAttribute("JT_USER",user); //存入到request域中 1.利用request对象传参
//2.利用ThreadLocal的方式动态传参
UserThreadLocal.set(user); //后面书写UserThreadLocal类..
return true; //表示放行
}
//3.将无效的cookie删除.
CookieUtil.deleteCookie("JT_TICKET","/","jt.com",response);
}
//重定向到用户的登录页面.
response.sendRedirect("/user/login.html");
return false;
}
/**
* 说明:利用拦截器方法清空数据!!很重要若不设置容易引发内存溢出!!
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//1.删除Request对象的数据
request.removeAttribute("JT_USER");
//2.删除ThreadLocal中的数据
UserThreadLocal.remove();
}
}
ThreadLocal介绍–本地线程变量
作用: 在多线程条件下使用ThreadLocal可以实现现场内数据的共享(同一线程内数据共享)
ThreadLocal API
public class UserThreadLocal { //静态方法,用时直接类名点调用即可
//1.定义变量
private static ThreadLocal<User> thread = new ThreadLocal<>();
public static void set(User user){ //赋值
thread.set(user);
}
public static User get(){ //取值
return thread.get();
}
public static void remove(){ //移除
thread.remove();
}
}
问: 在JT-WEB拦截器中使用ThreadLocal进行数据传参,
问题1:JT-WEB的Controller中能否获取数据? 可以
问题2:jt-cart的Service层中能否获取数据 不可以
因为:jt-web服务器与jt-cart的服务器的通讯是通过Dubbo框架的RPC实现的. RPC相当于开启了一个新的线程,所以无法通讯.
1.在同一个线程内可以使用ThreadLocal
2.“一般条件下” 在同一个tomcat内的线程为同一个线程.
========
实现京淘项目权限控制
分析:
如果用户没有进行登录操作时,访问购物车/订单等敏感操作时将不允许访问,应该重定向到系统的登录页面.
=====
订单项目实现
1 项目创建jt-order
2 pom文件中添加继承/依赖/插件
3 yml文件中配置 --添加dubbo框架的配置(指定dubbo的包路径)
server:
port: 8095
servlet:
context-path: /
spring:
datasource:
#driver-class-name: com.mysql.jdbc.Driver
#如果需要项目发布,则数据库地址必须配置远程数据库
url: jdbc:mysql://127.0.0.1:3306/jtdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
username: root
password: root
#配置视图解析器
mvc:
view:
prefix: /WEB-INF/views/
suffix: .jsp
#mybatis-plush配置
mybatis-plus:
type-aliases-package: com.jt.pojo
mapper-locations: classpath:/mybatis/mappers/*.xml
configuration:
map-underscore-to-camel-case: true
#日志记录 输出数据库的日志信息.
logging:
config: classpath:logging-config.xml
level:
com.jt.mapper: debug
dubbo:
scan:
basePackages: com.jt #指定dubbo的包路径
application:
name: provider-order #指定服务名称(必须指定)
registry:
address: zookeeper://192.168.126.129:2181?backup=192.168.126.129:2182,192.168.126.129:2183
protocol: #指定协议
name: dubbo #使用dubbo协议(tcp-ip) web-controller直接调用sso-Service
port: 20883 #每个服务都应该有自己特定的端口
说明
以上三个配置没有默认值,必须要给出具体的配置。
dubbo.scan.basePackages的值是开发的代码中含有com.alibaba.dubbo.config.annotation.Service和com.alibaba.dubbo.config.annotation.Reference注解所在的包。多个包之间用逗号隔开。
dubbo.registry.address的值前缀必须以 zookeeper://(nacos://) 开头,后面的 IP 地址和端口指的是 zookeeper/Nacos Server 的具体地址ip:port。
服务提供者注册到zookeeper注册中心中,服务消费者 从注册中心中拉取服务列表,负载均衡等调用相应服务器的对应方法实现具体功能.
=======
订单确认页面跳转
分析:
当用户点击去结算时,应该跳转到订单确认页面 order-cart.jsp,应该展现用户的购物车相关信息.之后提交订单即可.
1 编辑OrderController
package com.jt.controller;
@Controller
@RequestMapping("/order")
public class OrderController {
@Reference(check = false,timeout = 3000)
private DubboCartService cartService;
/**
* 订单确认页面跳转
* 1.url地址:http://www.jt.com/order/create.html
* 2.参数说明: 暂时没有
* 3.返回值: order-cart.jsp页面
* 4.页面的取值: ${carts}
*/
@RequestMapping("/create")
public String findCartByUserId(Model model){ //request域
Long userId = UserThreadLocal.get().getId();
List<Cart> cartList = cartService.findCartListByUserId(userId);
model.addAttribute("carts",cartList);
return "order-cart";
}
}
订单表说明
订单表注意事项
1.订单的ID号不是主键自增
2.订单状态信息由 1-6 注意状态说明
SpringMVC为对象的引用赋值
问题:如何解决重名提交的问题???
方案: 可以将对象进行封装,之后采用对象引用赋值的方式实现该功能.
注意事项: 属性的名称必须一致,否则赋值失败.
1 页面信息
<input type="text" name="name" value="二郎神"/>
<input type="text" name="age" value="3500"/>
<input type="text" name="dog.name" value="啸天犬"/>
<input type="text" name="dog.age" value="5000"/>
2 服务端接收
public class User {
private String name;
private Integer age;
private Dog dog;
}
public class Dog{
private String name;
private Integer age;
}
public String saveUser(User user){....}
订单对象定义
采用为对象引用赋值的操作. 但是该属性不属于数据库,所以需要额外的配置.
====
订单入库
分析:
当用户点击提交订单时要求实现三张表同时入库业务功能,并且保证orderId的值是一致的.
页面URL请求
页面JS分析
jQuery.ajax( {
type : "POST",
dataType : "json",
url : "/order/submit",
//key=value&key2=value2&....
data : $("#orderForm").serialize(), //serialize方法是将整个form表单信息提交
cache : false,
success : function(result) {
if(result.status == 200){
location.href = "/order/success.html?id="+result.data;
}else{
$("#submit_message").html("订单提交失败,请稍后重试...").show();
}
},
error : function(error) {
$("#submit_message").html("亲爱的用户请不要频繁点击, 请稍后重试...").show();
}
});
1 编辑OrderController
/**
* 完成订单业务提交
* 1.url地址: /order/submit
* 2.页面参数: 整个form表单
* 3.返回值结果: SysResult对象(orderId). json数据
*/
@RequestMapping("/submit")
@ResponseBody
public SysResult submit(Order order){
//利用拦截器的方式赋值.
Long userId = UserThreadLocal.get().getId();
order.setUserId(userId);
//1.完成订单入库,并且返回orderId
String orderId = orderService.saveOrder(order);
if(StringUtils.isEmpty(orderId)){
return SysResult.fail();
}
return SysResult.success(orderId);
}
2 编辑OrderService
package com.jt.service;
@Service
public class OrderServiceImpl implements DubboOrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderShippingMapper orderShippingMapper;
@Autowired
private OrderItemMapper orderItemMapper;
@Transactional//控制事务
@Override
public String saveOrder(Order order) {
//orderId由登录用户id+当前时间戳手动拼接.
String orderId = ""+ order.getUserId() + System.currentTimeMillis();
//1.完成订单入库操作
order.setOrderId(orderId).setStatus(1);
orderMapper.insert(order);
//2.完成订单商品入库
List<OrderItem> orderItemList = order.getOrderItems();
//insert into tb_order_item values(xxxxxx),(xxxxxxx),(xxxxxxx);
for (OrderItem orderItem : orderItemList){
orderItem.setOrderId(orderId);
orderItemMapper.insert(orderItem);
}
//3.完成订单物流入库
OrderShipping orderShipping = order.getOrderShipping();
orderShipping.setOrderId(orderId);
orderShippingMapper.insert(orderShipping);
System.out.println("完成订单入库操作!!!");
return orderId;
}
}
=================
订单查询
分析:
业务说明: 根据orderId号,检索订单的数据信息.要求利用Order对象将所有的数据一起返回.
1 编辑OrderController
/**
* 实现订单的查询,返回order对象
* url: http://www.jt.com/order/success.html?id=191600674802663
* 参数: id 订单的编号
* 返回值: success
* 页面取值: ${order.orderId}
*/
@RequestMapping("/success")
public String findOrderById(String id,Model model){
Order order = orderService.findOrderById(id);
model.addAttribute("order",order);
return "success";
}
2 编辑OrderService
@Override
public Order findOrderById(String id) {
Order order = orderMapper.selectById(id);
OrderShipping orderShipping = orderShippingMapper.selectById(id);
QueryWrapper<OrderItem> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_id",id);
List<OrderItem> orderItems = orderItemMapper.selectList(queryWrapper);
return order.setOrderShipping(orderShipping).setOrderItems(orderItems);
}
=================
超时订单的处理
说明:如果订单提交之后如果30分钟没有完成付款,则将订单状态改为6.表示订单交易关闭.
问题:如何实现每个订单30分钟超时呀???
思路1: 利用数据库的计时的函数每当order入库之后,可以添加一个函数30分钟之后修改状态.
该方法不好, 100万的订单刚刚入库. 100万个监听的事件.数据库压力很大
思路2: 利用消息队列的方式实现 redis 开启线程向redis中存储数据之后设定超时时间.当key一旦失效则修改数据库状态.
Redis主要做缓存使用. 但是不合适.
思路3:
开启单独的一个线程(异步),每隔1分钟查询一次数据库,修改超时的订单处理即可.
Quartz介绍
Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合也可以单独使用。Quartz可以用来创建简单或为运行十个,百个,甚至是好几万个Jobs这样复杂的程序。Jobs可以做成标准的Java组件或 EJBs。Quartz的最新版本为Quartz 2.3.2。
组件说明:
- Job 是用户自定义的任务.
- JobDetail 负责封装任务的工具API.如果任务需要被执行,则必须经过jobDetail封装.
- 调度器: 负责时间监控,当任务的执行时间一到则交给触发器处理
- 触发器: 当接收到调度器的命令则开启新的线程执行job…
1 导入jar包
<!--添加Quartz的支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
2 编辑配置类
package com.jt.config;
@Configuration
public class OrderQuartzConfig {
/**
* 思想说明:
* 如果需要执行定时任务需要考虑的问题
* 1.任务多久执行一次. 1分钟执行一次
* 2.定时任务应该执行什么
*/
//定义任务详情
@Bean
public JobDetail orderjobDetail() {
//指定job的名称和持久化保存任务
return JobBuilder
.newJob(OrderQuartz.class) //引入自定义的任务//1.定义执行的任务
.withIdentity("orderQuartz")//指定任务名称//2.任务指定名称
.storeDurably()
.build();
}
//定义触发器
@Bean
public Trigger orderTrigger() {
/*SimpleScheduleBuilder builder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInMinutes(1) //定义时间周期
.repeatForever();*/
//通过调度器,指定程序多久执行一次.
//0 0/1 * * * ? 时间表达式 规定任务多久执行一次
CronScheduleBuilder scheduleBuilder
= CronScheduleBuilder.cronSchedule("0 0/1 * * * ?");
return TriggerBuilder
.newTrigger()
.forJob(orderjobDetail())
.withIdentity("orderQuartz")
.withSchedule(scheduleBuilder).build();
}
}
3 编辑定时任务
package com.jt.quartz;
//准备订单定时任务
@Component
public class OrderQuartz extends QuartzJobBean {
@Autowired
private OrderMapper orderMapper;
/**
* 当规定的执行时间一到,触发器就会开启线程,执行指定的任务.
* 业务需求:
* 要求将超时订单关闭. 要求30分钟 status 由1改为6
* 如何定义超时:
* now() - created > 30分钟 订单超时
* created < now -30
* Sql:
* update tb_order set status = 6,updated = #{updated}
* where created < (now -30) and status = 1;
*/
@Transactional
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
//对时间进行计算//1.利用java工具API日历对象Calendar完成计算
Calendar calendar = Calendar.getInstance(); //实例化对象 获取当前时间
calendar.add(Calendar.MINUTE, -30);
Date timeOut = calendar.getTime(); //获取超时时间
Order order = new Order();
order.setStatus(6);
UpdateWrapper<Order> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("status", 1)
.lt("created",timeOut);
orderMapper.update(order , updateWrapper);
System.out.println("定时任务执行成功!!!!");
}
}
======================
篇幅有点大,后期会优化一些
后面会对SpringCloud微服务API集合做介绍