Spring高级技术应用——百战商城实现(下)

2020-07-27 11:33:12 浏览数 (1)

六、开发百战商城前台系统

基于Solr的搜索服务的数据导入

环境搭建

  1. 安装Solr与中文分词器 Solr与中文分词器的安装
  2. 指定需要被搜索字段 例如:item_title, item_sell_point等, 以便我们能通过这些字段下的关键词来进行搜索
代码语言:javascript复制
			 <!-- type:类型,text_ik代表使用中分词器
   		      indexed:是否参与索引,true:是
			  stored:是否进行数据返回/回显,true:是
			  item_keywords:父作用域,会将其下面子作用域进行扫描,提高读取速度,但是需要注意的是必须要指定上面的作用域-->
	<field name="item_title" type="text_ik" indexed="true" stored="true"/>
	<field name="item_sell_point" type="text_ik" indexed="true" stored="true"/>
	<field name="item_price" type="long" indexed="true" stored="true"/>
	<field name="item_image" type="string" indexed="false" stored="true" />
	<field name="item_category_name" type="string" indexed="true" stored="true" />
	<field name="item_desc" type="text_ik" indexed="true" stored="false" />
	<field name="item_keywords" type="text_ik" indexed="true" stored="false" multiValued="true"/>
	
	<copyField source="item_title" dest="item_keywords"/>
	<copyField source="item_sell_point" dest="item_keywords"/>
	<copyField source="item_category_name" dest="item_keywords"/>
	<copyField source="item_desc" dest="item_keywords"/>
  1. 创建搜索项目 ,修改pom文件

需要用到pojo,但是我们可以通过依赖Mapper项目来简介添加Pojo项目

需要用到Spring Data整合Solr的坐标

代码语言:javascript复制
<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>
	<parent>
		<groupId>ah.szxy.project</groupId>
		<artifactId>bz_parent</artifactId>
		<version>0.0.1-SNAPSHOT</version>
	</parent>
	<artifactId>frontend_search</artifactId>

	<dependencies>
		<!-- mapper(include pojo) -->
		<dependency>
			<groupId>ah.szxy.project</groupId>
			<artifactId>common_mapper</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>
		<!-- untils -->
		<dependency>
			<groupId>ah.szxy.project</groupId>
			<artifactId>common_utils</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>

		<!--Spring Boot Web Starter -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!--Spring Cloud Eureka Client Starter -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
		<!--Spring Boot Data Solr Starter -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-solr</artifactId>
		</dependency>
		
	</dependencies>
	
	<!-- 打包插件,打成jar,用于在虚拟机中发布服务 -->
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

 
  1. 修改全局配置文件 application.yml 配置应用名,数据库连接参数,Solr的配置,端口号,注册中心服务端地址
代码语言:javascript复制
spring:
  application:
    name: frontend-search
  datasource:
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/bz_shop?useSSL=false
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource   
  data:
    solr:
      host: http://192.168.179.138:8080/solr/
      core: collection1
      
server:
  port: 9900
  
eureka:       #配置Eureka服务注册中心地址
  client:
    serviceUrl:
      defaultZone: http://eureka-server:8761/eureka/ 
  1. 启动类
代码语言:javascript复制
/**
 * 搜索服务的启动类
 * @author 曹海洋
 *
 */
@SpringBootApplication
@MapperScan("ah.szxy.mapper")
public class FrontendSearchApplication {
	
	public static void main(String[] args) {
		SpringApplication.run(FrontendSearchApplication.class, args);
	}
}
  1. 配置类
代码语言:javascript复制
/**
* SolrTemplate 配置类
* 注意包
*/
@Configuration
public class SolrConfiguration {
	@Autowired
	private SolrClient solrClient;
	
	@Bean
	public SolrTemplate	 getSolrTemplate() {
		return new SolrTemplate(solrClient);
	}
}

数据库查询语句构建

1.设计查询语句( 查询到的结果用于导入Solr索引库中 )

数据库查询的参数是根据Solr中指定的字段来写的 ,因为Solr已经我们添加了 Id 这个字段,

所以这里没有配置Id 字段,但是数据库的查询语句中是有id这个字段的

查询语句的定义(多表连接查询)

代码语言:javascript复制
select item.id,item.title,item.sell_point,item.price,item.image,cat.name,idesc.item_desc
from tb_item item
join tb_item_cat cat
on item.cid = cat.id
join tb_item_desc idesc
on item.id = idesc.item_id

查询结果

  1. 创建Mapper层和实体类 因为使用的是逆向工程生成的查询方法 ,只能进行单表查询 , 如果需要使用多表连接查询需要我们自定义这些实体类,Mapper接口以及映射文件
代码语言:javascript复制
/**
*实体类
 * item.id,item.title,item.sell_point,item.price,item.image,
 *  cat.name,idesc.item_desc
 *  
 * @author chy
 *
 */
public class SolrItem implements Serializable {
	private Long id;
	private String title;
	private String sell_point;
	private Long price;
	private String image;
	private String name;
	private String item_desc;
	public Long getId() {
		return id;
	}
	public void setId(Long id) {
		this.id = id;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getSell_point() {
		return sell_point;
	}
	public void setSell_point(String sell_point) {
		this.sell_point = sell_point;
	}
	public Long getPrice() {
		return price;
	}
	public void setPrice(Long price) {
		this.price = price;
	}
	public String getImage() {
		return image;
	}
	public void setImage(String image) {
		this.image = image;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getItem_desc() {
		return item_desc;
	}
	public void setItem_desc(String item_desc) {
		this.item_desc = item_desc;
	}

}
  1. Mapper接口
代码语言:javascript复制
/**
 * 定义获取查询结果的接口
 * @author chy
 *
 */
public interface SolrItemMapper {
	List<SolrItem> getItemList();
}
  1. 映射文件 注意: 这里执行的是查询操作, 在搜索的项目中执行插入的操作
代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="ah.szxy.mapper.SolrItemMapper">

	<select id="getItemList" resultType="ah.szxy.pojo.SolrItem">
		select item.id,item.title,item.sell_point,item.price,item.image,cat.name,idesc.item_desc
		from tb_item item
		join tb_item_cat cat
		on item.cid = cat.id
		join tb_item_desc idesc
		on item.id = idesc.item_id
	</select>
</mapper>

将数据导入Solr

  1. 业务层接口 返回值可以自定义,可以定义一个Interger类型 public interface SolrService { /** * 向Solr中导入数据 * @return */ Result importAll(); }
  2. 接口实现类 SolrItemMapper 注入Mapper接口,solrTemplate以及配置文件中的索引库的指定spring.data.solr.core 这样做的目的是防止硬编码
代码语言:javascript复制
import org.apache.solr.common.SolrInputDocument;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.solr.core.SolrTemplate;
import org.springframework.stereotype.Service;

import ah.szxy.mapper.SolrItemMapper;
import ah.szxy.pojo.SolrItem;
import ah.szxy.utils.Result;

@Service
public class SolrServiceImpl implements SolrService {
	@Autowired
	private SolrItemMapper solrItemMapper;
	@Autowired
	private SolrTemplate solrTemplate;

	@Value("${spring.data.solr.core}")
	private String collection;

	@Override
	public Result importAll() {
		try {
			// 查询数据
			List<SolrItem> list = this.solrItemMapper.getItemList();
			// 将数据添加索引库中
			for (SolrItem item : list) {
				// 创建SolrInputDocument 对象
				SolrInputDocument document = new SolrInputDocument();
				document.setField("id", item.getId());
				document.setField("item_title", item.getTitle());
				document.setField("item_sell_point", item.getSell_point());
				document.setField("item_price", item.getPrice());
				document.setField("item_image", item.getImage());
				document.setField("item_category_name", item.getName());
				document.setField("item_desc", item.getItem_desc());
				// 写入索引库
				this.solrTemplate.saveDocument(this.collection, document);
			}
			this.solrTemplate.commit(this.collection);
			return Result.ok();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return Result.error("导入失败");
	}
}
  1. controller
代码语言:javascript复制
@RestController
@RequestMapping("/solr/search")
public class SolrController {
	@Autowired
	private SolrService solrService;
	
	@RequestMapping("/importAll")
	public Result importAll() {
		return this.solrService.importAll();
	}
}

4.启动项目

访问 :导入数据的这个方法,查看返回结果

访问 http://192.168.179.138:8080/solr/#/ ,没有导入数据前

导入数据后, 出现下面结果说明导入成功:

基于Solr的搜索服务的实现

接口文档

根据接口文档知,我们需要创建一个返回给前端页面的数据模型,包含下面的7条分页属性( 也是我们在Solr的配置文件中所配置的属性 )

代码语言:javascript复制
/**
* Solr 返回给前端页面的数据模型
* 根据前端接口文档编写
*/
public class SolrDocument implements Serializable {
	private String id;
	private String item_title;
	private String item_sell_point;
	private String item_price;
	private String item_image;
	private String item_category_name;
	private String item_desc;
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getItem_title() {
		return item_title;
	}
	public void setItem_title(String item_title) {
		this.item_title = item_title;
	}
	public String getItem_sell_point() {
		return item_sell_point;
	}
	public void setItem_sell_point(String item_sell_point) {
		this.item_sell_point = item_sell_point;
	}
	public String getItem_price() {
		return item_price;
	}
	public void setItem_price(String item_price) {
		this.item_price = item_price;
	}
	public String getItem_image() {
		return item_image;
	}
	public void setItem_image(String item_image) {
		this.item_image = item_image;
	}
	public String getItem_category_name() {
		return item_category_name;
	}
	public void setItem_category_name(String item_category_name) {
		this.item_category_name = item_category_name;
	}
	public String getItem_desc() {
		return item_desc;
	}
	public void setItem_desc(String item_desc) {
		this.item_desc = item_desc;
	}
	
}

功能具体实现

1.接口类

在SpringData整合Solr定义的分页属性中page的类型就是Long而不是Integer

代码语言:javascript复制
  /**
    * 首页搜索功能的实现
    * 
    * @param q 关键词
    * @param page 在SpringData整合Solr定义的page的类型就是Long而不是Integer
    * @param rows
    * @return
    */
   List<SolrDocument> selectByq(String q,Long page,Integer rows);

2.接口实现类

代码语言:javascript复制
	/**
	 * 搜索solr索引库
	 * 
	 * import org.springframework.data.solr.core.query.Criteria;
	 */
	@Override
	public List<SolrDocument> selectByq(String q, Long page, Integer rows) {

		// 设置高亮查询条件(使用我们之前在配置文件中定义的复合域/父作用域)
		HighlightQuery query = new SimpleHighlightQuery();
		Criteria criteria = new Criteria("item_keywords");
		criteria.is(q);
		query.addCriteria(criteria);

		// 设置高亮属性
		HighlightOptions highlightOptions = new HighlightOptions();
		highlightOptions.addField("item_title");// 设置高亮显示的域
		highlightOptions.setSimplePrefix("<em style='color:red'>");// 设置高亮的样式的前缀
		highlightOptions.setSimplePostfix("</em>");// 后缀
		query.setHighlightOptions(highlightOptions);// 将高亮属性设置封装到查询条件中

		// 分页(调用Solr自带的分页方法)
		query.setOffset((page - 1) * rows);
		query.setRows(rows);
		
		/**
		 * HighlightPage: 是一个封装了所有查询的结果集的对象
		 * queryForHighlightPage三个属性 : 指定索引库,指定查询条件 , 将查询的结果封装成指定的对象
		 */
		HighlightPage<SolrDocument> highlightPage = this.solrTemplate.queryForHighlightPage(this.collection, query,
				SolrDocument.class);
		List<HighlightEntry<SolrDocument>> highlighted = highlightPage.getHighlighted();

		for (HighlightEntry<SolrDocument> tbItemHighlightEntry : highlighted) {
			SolrDocument entity = tbItemHighlightEntry.getEntity();// 实体对象,原始的实体对象
			List<HighlightEntry.Highlight> highlights = tbItemHighlightEntry.getHighlights();//标识为高亮的部分
			// 如果有高亮,就做高亮处理
			if (highlights != null && highlights.size() > 0 && highlights.get(0).getSnipplets().size() > 0) {
				entity.setItem_title(highlights.get(0).getSnipplets().get(0));//设置高亮
			}
		}

		List<SolrDocument> list = highlightPage.getContent();
		return list;
	}

3.controller

指定分页的默认属性

代码语言:javascript复制
	/**
	 * 首页带高亮搜索
	 * 这里的分页是自己指定的,而不是前端控制的 , 所以也就没使用value属性进行校正
	 * 
	 * @param q
	 * @param page 
	 * @param rows
	 * @return
	 */
	@RequestMapping("/list")
	public List<SolrDocument> selectByq(String q,@RequestParam(defaultValue="1") Long page
			 ,@RequestParam(defaultValue="10") Integer rows){
		
		try {
			return this.solrService.selectByq(q, page, rows);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

启动项目, 访问前端页面, 测试关键词索引的效果

购物车功能设计——未登录(Cookie)

购物车的添加——复杂逻辑封装成方法的体现

模仿京东设计,

在没有登录时, 我们是可以添加商品到购物车的(保存到浏览器的Cookie中) ,

但是当我们登录以后, 购物车中的商品会被同步到自己账号下的购物车, 并且无登录下的清空购物车

1.创建项目(继承父项目) ,修改pom文件

代码语言:javascript复制
<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>
	<parent>
		<groupId>ah.szxy.project</groupId>
		<artifactId>bz_parent</artifactId>
		<version>0.0.1-SNAPSHOT</version>
	</parent>
	<artifactId>frontend_cart</artifactId>

	<dependencies>
		<!--Spring Boot Web Starter -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!--Spring Cloud Eureka Client Starter -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
		<!--Spring Cloud OpenFeign Starter -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-openfeign</artifactId>
		</dependency>

		<!-- pojo -->
		<dependency>
			<groupId>ah.szxy.project</groupId>
			<artifactId>common_pojo</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>
		<!-- utils -->
		<dependency>
			<groupId>ah.szxy.project</groupId>
			<artifactId>common_utils</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>
	</dependencies>


	<!-- 打包插件,打成jar,用于在虚拟机中发布服务 -->
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

2.修改全局配置文件

设置临时购物车缓存到Cookie中的key

代码语言:javascript复制
spring:       #配置应用名,数据库连接参数
  application:
    name: frontend_cart
    
server:       #配置端口号
  port: 9040
  
eureka:       #配置Eureka服务注册中心地址
  client:
    serviceUrl:
      defaultZone: http://eureka-server:8761/eureka/
      
#设置临时购物车缓存到Cookie中的key
cart_cookie_key: CART_COOKIE_KEY

3.创建启动类

代码语言:javascript复制
/**
 * 购物车启动类
 * @author chy
 *
 */
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class CartApplication {
	
	public static void main(String[] args) {
		SpringApplication.run(CartApplication.class, args);
	}
}

4.Controller

StringUtils.isBlank:

可以一次性检验字符串(1)是否为null,(2)是否为空字符串(引号中间有空格) 如: " " , (3)是否为 “”, 比isEmpty多了空字符串的校验

代码语言:javascript复制
@RestController
@RequestMapping("/cart")
public class CartController {
	@Autowired
	private CookieCartService cookieCartService;
	
	/**
	 * 将商品添加到购物车
	 * 需要判断是否登录
	 * @param userId 用户id
	 * @param itemId 商品id
	 * @param num 商品数据
	 * @param request 
	 * @param response
	 * @return
	 */
	@RequestMapping("/addItem")
	public Result addItem(String userId,Long itemId,@RequestParam(defaultValue="1")Integer num
			,HttpServletRequest request, HttpServletResponse response) {
		
		try {
			if (StringUtils.isBlank(userId)) {
				//执行用户未登录状态下的业务逻辑
				return this.cookieCartService.addItem(itemId, num, request, response);
			}else {
				//执行用户登录状态下的业务逻辑
				
				
				
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return Result.error(null);
	}
}

5.Feign接口类

用于调用根据id查询商品的服务, 因为只有查到了商品才能对其进行添加修改等操作

代码语言:javascript复制
@FeignClient("common-item")
public interface CommonItemFeignClient {
	
	//-------------------------/service/item---------------------------
	@PostMapping("/service/item/selectItemInfo")
	TbItem selectItemInfo(@RequestParam("itemId") Long itemId);
}

7.创建购物车模型

根据购物车列表接口添加相关属性

代码语言:javascript复制
/**
 * 商品购物车模型
 * 根据购物车列表接口添加相关属性
 * @author chy
 *
 */
public class CartItem implements Serializable{
	
	private Long id;
	private String title;
	private String sellPoint;
	private String image;
	private int num;
	private Long price;
	public Long getId() {
		return id;
	}
	public void setId(Long id) {
		this.id = id;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getSellPoint() {
		return sellPoint;
	}
	public void setSellPoint(String sellPoint) {
		this.sellPoint = sellPoint;
	}
	public String getImage() {
		return image;
	}
	public void setImage(String image) {
		this.image = image;
	}
	public int getNum() {
		return num;
	}
	public void setNum(int num) {
		this.num = num;
	}
	public Long getPrice() {
		return price;
	}
	public void setPrice(Long price) {
		this.price = price;
	}
	
	
}

7.接口类

因为是未登录,所以用到商品的id,商品的数量,HttpServletRequest ,HttpServletResponse 属性,而没有userId

代码语言:javascript复制
/**
 * 未登录添加购物车逻辑
 * @author chy
 *
 */
public interface CookieCartService {
	/**
	 * 添加商品到购物车
	 * @param itemId
	 * @param num
	 * @param request 调用Cookie使用
	 * @param response 调用Cookie使用
	 * @return
	 */
	public Result addItem(Long itemId,Integer num,
			HttpServletRequest request, HttpServletResponse response);
}

8.接口实现类

这里注入并调用了在全局配置文件中的设置临时购物车缓存到Cookie中的key

用户未登录状态下的购物车操作业务 (将复杂的逻辑封装成方法,分步实现)

1.获取临时购物车( 从Cookie中获取 )

2.查询商品

3.向购物车中添加商品

4.将购物车通过Cookie写回给浏览器

对1: 如果临时购物车存在,则将其转换成Map输出 ,如果不存在,则创建一个Map集合 , 然后返回这个map

对2: 通过Feign直接调用相关业务

对3: 从购物车的map中查看是否有这个商品 , 如果有,则直接修改数量 : 如果没有,则根据购物车模型新建一个购物车商品对象放入到map中

对4: 将购物车对象转换转换成json格式, 放入Cookie中

代码语言:javascript复制
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import ah.szxy.frontend.cart.feign.CommonItemFeignClient;
import ah.szxy.frontend.cart.service.CookieCartService;
import ah.szxy.pojo.TbItem;
import ah.szxy.utils.CartItem;
import ah.szxy.utils.CookieUtils;
import ah.szxy.utils.JsonUtils;
import ah.szxy.utils.Result;

@Service
public class CookieCartServiceImpl implements CookieCartService {

	@Autowired
	private CommonItemFeignClient commonItemFeignClient;
	@Value("${cart_cookie_key}")
	private String cart_cookie_key;
	
	
	/**
	 *用户未登录状态下的购物车操作业务
	 *
	 *将复杂的逻辑封装成方法,分步实现
	 *1.获取临时购物车( 从Cookie中获取 )
	 *2.查询商品
	 *3.向购物车中添加商品
	 *4.将购物车通过Cookie写回给浏览器
	 */
	@Override
	public Result addItem(Long itemId, Integer num, HttpServletRequest request, HttpServletResponse response) {

		// 1.获取临时购物车( 从Cookie中获取 )
		Map<String, CartItem>cart= this.getTempCart(request);
		// 2.查询商品
		TbItem item= this.commonItemFeignClient.selectItemInfo(itemId);
		// 3.向购物车中添加商品
		this.addItemToCart(cart,item,num,itemId);
		// 4.将购物车通过Cookie写回给浏览器
		this.addCookieToClient(request,response,cart);
		
		return Result.ok();
	}

	
	/**
	 * 1.获取临时购物车( 从Cookie中获取 )思路:
	 * 
	 * 如果临时购物车存在,则将其转换成Map输出
	 * 如果不存在,则创建一个Map集合 , 然后返回这个map
	 * 
	 * StringUtils.isBlank:可以一次性检验字符串(1)是否为null,(2)是否为空字符串(引号中间有空格) 如: " " , (3)是否为 "",比isEmpty多了空字符串的校验
	 * @param request
	 * @return
	 */
	private Map<String, CartItem> getTempCart(HttpServletRequest request) {
		//从Cookie中获取Json格式购物车信息
		String cartJsonStr=CookieUtils.getCookieValue(request, this.cart_cookie_key,true);//true使用编码.开启后可以存放中文
		
		if (StringUtils.isBlank(cartJsonStr)) {
			//如果临时购物车不存在,抛出一个空的map集合
			return new HashMap<String, CartItem>();
		}else {
			try {
				//如果临时购物车存在,那么就将其转换成map,便于后续的添加商品操作
				Map<String, CartItem>map =JsonUtils.jsonToMap(cartJsonStr, CartItem.class);  //beanType:指定需要转换的模型 CartItem:商品购物车模型
				return map;
			} catch (Exception e) {
				e.printStackTrace();
			}
			
		}
		
		return new HashMap<String, CartItem>();//购物车不存在的情况,返回这个购物车的对象
	}
    
	/**
	 * 3.向购物车中添加商品
	 * 
	 * 从购物车的map中查看是否有这个商品
	 * 如果有,则直接修改数量
	 * 如果没有,则根据购物车模型新建一个购物车商品对象放入到map中
	 * @param cart
	 * @param item
	 * @param num
	 * @param itemId
	 */
	private void addItemToCart(Map<String, CartItem> cart, TbItem item, Integer num, Long itemId) {
		//从购物车中查询商品
		CartItem cartItem= cart.get(itemId.toString());//map.get(key): 通过cart的key(toString)获取它的value
		if (cartItem==null) {
			//没有相同的商品( 根据购物车模型创建 )
			CartItem c = new CartItem();
			c.setId(item.getId());
			c.setImage(item.getImage());
			c.setNum(num);
			c.setPrice(item.getPrice());
			c.setSellPoint(item.getSellPoint());
			c.setTitle(item.getTitle());
			cart.put(item.getId().toString(),cartItem);//map.put(key,value)
		}else {
			//如果存在相同商品
			cartItem.setNum(cartItem.getNum() num);
			
		}
	}
	
	/**
	 * 4.将购物车通过Cookie写回给浏览器
	 * 
	 * 将购物车对象转换转换成json格式, 放入Cookie中
	 * @param request
	 * @param response
	 * @param cart 
	 */
	private void addCookieToClient(HttpServletRequest request, 
			HttpServletResponse response, Map<String, CartItem> cart) {
		
		//将购物车对象转换转换成json格式
		String cartJsonStr = JsonUtils.objectToJson(cart);
		//将Json串放入Cookie中
		CookieUtils.setCookie(request, response, this.cart_cookie_key, cartJsonStr, true);
	}
}

浏览器效果

查询购物车列表——遍历map集合,将map中的元素放入的list中

1.controller

代码语言:javascript复制
/**
	 * 查询购物车列表
	 * @param userId
	 * @param request
	 * @param response
	 * @return
	 */
	@RequestMapping("/showCart")
	public Result showCart(String userId,HttpServletRequest request, HttpServletResponse response) {
		try {
			if (StringUtils.isBlank(userId)) {
				//执行用户未登录状态下的业务逻辑
				return this.cookieCartService.showCart(request, response);
			}else {
				//执行用户登录状态下的业务逻辑
				
				
				
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return Result.error(null);
	}

2.业务层实现类

通过自定义的getTempCart方法获取购物车的map对象,遍历map集合,

将每个元素赋值给一个list集合,然后将这个list集合返回

代码语言:javascript复制
/**
	 * 查询购物车列表
	 */
	@Override
	public Result showCart(HttpServletRequest request, HttpServletResponse response) {
		//创建返回值对象
		List<CartItem>list=new ArrayList<CartItem>();
		
		//从Cookie中获取购物车对象
		Map<String, CartItem>cartMap=this.getTempCart(request);
		//遍历map集合,将map中的元素放入的list中
		Set<String> keys = cartMap.keySet();
		for (String key : keys) {
			list.add(cartMap.get(key));
		}
		
		return Result.ok(list);
	}

修改购物车中商品的数量——操作Cookie

1.controller

代码语言:javascript复制
	/**
	 * 修改购物车中商品的数量
	 * @param userId
	 * @param itemId
	 * @param num
	 * @param request
	 * @param response
	 * @return
	 */
	@RequestMapping("/updateItemNum")
	public Result updateItemNum(String userId,Long itemId,@RequestParam(defaultValue="1")Integer num
			,HttpServletRequest request, HttpServletResponse response) {
		
		try {
			if (StringUtils.isBlank(userId)) {
				//执行用户未登录状态下的业务逻辑
				return this.cookieCartService.updateItemNum(itemId, num, request, response);
			}else {
				//执行用户登录状态下的业务逻辑
				
				
				
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return Result.error(null);
	}

2.接口实现类

获取购物车的map对象,通过key获取商品对象,为其数量属性赋值,然后将map返回给Cookie

代码语言:javascript复制
/**
	 * 更新购物车商品数量
	 */
	@Override
	public Result updateItemNum(Long itemId, Integer num, 
			HttpServletRequest request, HttpServletResponse response) {
		
		//从Cookie中获取购物车对象,通过上面的方法
		Map<String, CartItem>cartMap=this.getTempCart(request);
		//从map中获取商品信息
		CartItem cartItem = cartMap.get(itemId.toString());
		//如果该商品不为空,更改数量信息
		if (cartItem!=null) {
			cartItem.setNum(num);
		}
		
		//将改变添加到Cookie中,通过上面的方法
		this.addCookieToClient(request, response, cartMap);
		return Result.ok();
	}

删除购物车中商品——操作Cookie

1.controller

代码语言:javascript复制
@RequestMapping("/deleteItemFromCart")
	public Result deleteItemFromCart(String userId,Long itemId,HttpServletRequest request, HttpServletResponse response) {
		
		try {
			if (StringUtils.isBlank(userId)) {
				//执行用户未登录状态下的业务逻辑
				return this.cookieCartService.deleteItemFromCart(itemId, request, response);
			}else {
				//执行用户登录状态下的业务逻辑
				
				
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return Result.error(null);
	}
}

2.业务层实现类

获取购物车的map对象,通过key删除即可,然后将map返回给Cookie

代码语言:javascript复制
/**
	 * 删除购物车中的商品
	 */
	@Override
	public Result deleteItemFromCart(Long itemId, HttpServletRequest request, HttpServletResponse response) {

		// 从Cookie中获取购物车对象,通过上面的方法
		Map<String, CartItem> cartMap = this.getTempCart(request);
		
		//通过key(itemId)删除商品信息
		cartMap.remove(itemId.toString());
		
		//将改变放到Cookie中
		this.addCookieToClient(request, response, cartMap);
		return Result.ok();
	}

购物车功能设计——登录(Redis)

在登陆状态下 ,会将购物车中的商品数据同步到redis中,

无论你退出多少次 ,登陆后再次点击购物车还是可以看到那些被同步的商品

购物车的添加——复杂逻辑封装成方法的体现

common_redis项目中

代码语言:javascript复制
@RestController
@RequestMapping("/redis/cart")
public class CartController {
	@Autowired
	private CartService cartService;
	/**
	 * 添加购物车缓存
	 * @param map 需要使用@RequestBody接收前端传来的json数据
	 */
	@RequestMapping("/insertCart")
	public void insertCart(@RequestBody Map<String, Object>map) {
		this.cartService.insertCart(map);
	}
	
	
	/**
	 *从redis缓存中查询购物车信息
	 * @param userId
	 * @return
	 */
	@RequestMapping("/selectCartByUserId")
	public Map<String, CartItem> selectCartByUserId(@RequestParam("userId")String userId){
		return this.cartService.selectCartByUserId(userId);
		
	}
}

2.接口类

代码语言:javascript复制
public interface SSOService {
	/**
	 * 将用户添加到缓存
	 * @param tbUser
	 * @param token
	 */
	void userLogin(TbUser tbUser,String userToken);
	/**
	 * 用户退出
	 * @param userToken
	 */
	void userLogout(String userToken);
}

3.接口实现类

在这里需要设置绑定用户信息和购物车信息的缓存的key

注意这里使用opsForHash 将数据放入到缓存redis中hash数据类型介绍(第三章第二节)

代码语言:javascript复制
#配置缓存购物车和用户信息key
frontend_cart_redis_key: frontend:cart:redis:key
代码语言:javascript复制
@Service
public class CartServiceImpl implements CartService {
	
	@Autowired
	private RedisTemplate<String, Object>redisTemplate;
	@Value("${frontend_cart_redis_key}")
	private String frontend_cart_redis_key;
	
	/**
	 * 添加购物车缓存
	 */
	@Override
	public void insertCart(Map<String, Object> map) {
		//获取用户id信息
		String userId=(String) map.get("userId");
		//获取购物车信息
		Map<String, CartItem> cart=(Map<String, CartItem>) map.get("cart");
		
		//opsForHash用来存放 root key,son key-value形式的数据
		//key:根key:frontend_cart_redis_key, 子key:userId , value: cart
		this.redisTemplate.opsForHash().put(this.frontend_cart_redis_key,userId, cart);
	}
	
	/**
	 * 根据userId查询购物车缓存信息
	 */
	@Override
	public Map<String, CartItem> selectCartByUserId(String userId) {
		//根据Hash类型的两个key查询对应的value
		return (Map<String, CartItem>) this.redisTemplate.opsForHash().get(this.frontend_cart_redis_key, userId);
	}

}

frontend_cart项目

新增三个部分, 对登陆后,购物车逻辑进行完善

1.feign接口

代码语言:javascript复制
@FeignClient("common-redis")
public interface CommonRedisFeignClient {
	
	//---------------------------/redis/cart-----------------------------------
	@PostMapping("/redis/cart/insertCart")
	void insertCart(@RequestBody Map<String, Object>map);
	
	@PostMapping("/redis/cart/selectCartByUserId")
	Map<String, CartItem> selectCartByUserId(@RequestParam("userId")String userId);
}

2.登陆状态下对购物车的操作

代码语言:javascript复制
/**
 * 登陆情况下-购物车操作
 * 
 * @author chy
 *
 */
public interface RedisCartService {
	/**
	 * 添加商品到购物车
	 * 
	 * @param itemId
	 * @param num
	 * @param userId
	 * @return
	 */
	Result addItem(Long itemId, Integer num, String userId);

	/**
	 * 查询购物车列表
	 * 
	 * @param userId
	 * @return
	 */
	Result showCart(String userId);
}

3.接口实现类

对复杂的业务逻辑, 我们可以封装成方法,

然后逐个解决 ,但是需要我们仔细斟酌的是返回值以及参数列表

代码语言:javascript复制
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import ah.szxy.frontend.cart.feign.CommonItemFeignClient;
import ah.szxy.frontend.cart.feign.CommonRedisFeignClient;
import ah.szxy.frontend.cart.service.RedisCartService;
import ah.szxy.pojo.TbItem;
import ah.szxy.utils.CartItem;
import ah.szxy.utils.Result;

@Service
public class RedisCartServiceImpl implements RedisCartService {

	@Autowired
	private CommonItemFeignClient commonItemFeignClient;
	@Autowired
	private CommonRedisFeignClient commonRedisFeignClient;

	/**
	 * 添加商品到购物车 思路: 查询商品->获取购物车->将商品添加到大购物车中->将购物车缓存到Redis中
	 */
	@Override
	public Result addItem(Long itemId, Integer num, String userId) {

		// 1.查询商品
		TbItem tbItem = this.commonItemFeignClient.selectItemInfo(itemId);

		// 2.获取购物车(Redis)
		Map<String, CartItem> cart = this.getCartToRedis(userId);

		// 3.将商品添加到购物车中
		this.addItem(itemId, num, tbItem, cart);

		// 4.将购物车缓存到Redis中
		this.addCartToRedis(userId, cart);

		return Result.ok();
	}

	/**
	 * 2.从redis中获取购物车
	 * 
	 * @param userId
	 * @return
	 */
	private Map<String, CartItem> getCartToRedis(String userId) {

		// 获取购物车
		Map<String, CartItem> cart = this.commonRedisFeignClient.selectCartByUserId(userId);
		// 判断购物车是否为空
		if (cart != null) {// 不为空返回
			return cart;
		} else {
			return new HashMap<String, CartItem>();// 为空新建一个
		}
	}

	/**
	 * 3.将商品添加到购物车中
	 * 
	 * @param itemId
	 * @param tbItem
	 * @param cart
	 * @return
	 */
	private void addItem(Long itemId, Integer num, TbItem tbItem, Map<String, CartItem> cart) {

		// 从购物车map中查询商品
		CartItem cartItem = cart.get(itemId.toString());
		if (cartItem != null) {
			// 不为空,直接改变商品数量
			cartItem.setNum(cartItem.getNum()   num);
		} else {
			// 为空,创建一个新的购物车商品模型
			CartItem c = new CartItem();
			c.setId(tbItem.getId());
			c.setImage(tbItem.getImage());
			c.setNum(num);
			c.setPrice(tbItem.getPrice());
			c.setSellPoint(tbItem.getSellPoint());
			c.setTitle(tbItem.getTitle());
			cart.put(tbItem.getId().toString(), c);// map.put(key,value)

		}
	}

	/**
	 * 4.将购物车缓存到Redis中
	 * 
	 * @param userId
	 * @param cart
	 */
	private void addCartToRedis(String userId, Map<String, CartItem> cart) {

		// 将购物车(map)和用户id(string)在放入一个map中
		Map<String, Object> map = new HashMap<>();
		map.put("userId", userId);
		map.put("cart", cart);
		this.commonRedisFeignClient.insertCart(map);

	}

	/**
	 * 查看购物车(登陆状态下)
	 */
	@Override
	public Result showCart(String userId) {
		// 创建返回值对象
		List<CartItem> list = new ArrayList<CartItem>();

		// 从Redis中获取购物车对象
		Map<String, CartItem> cartMap = this.getCartToRedis(userId);
		
		// 遍历map集合的key,将map中的元素放入的list中
		Set<String> keys = cartMap.keySet();
		for (String key : keys) {
			list.add(cartMap.get(key));
		}

		return Result.ok(list);
	}

}

4.controller

判断用户Id是否为空?补充不为空(if…else…的else)的部分

代码语言:javascript复制
	/**
	 * 将商品添加到购物车
	 * 需要判断是否登录
	 * @return
	 */
	@RequestMapping("/addItem")
	public Result addItem(String userId,Long itemId,@RequestParam(defaultValue="1")Integer num
			,HttpServletRequest request, HttpServletResponse response) {
		
		try {
			if (StringUtils.isBlank(userId)) {
				//执行用户未登录状态下的业务逻辑
				return this.cookieCartService.addItem(itemId, num, request, response);
			}else {
				
				//执行用户登录状态下的业务逻辑
				return this.redisCartService.addItem(itemId, num, userId);
				
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return Result.error(null);
	}
	
	/**
	 * 查询购物车列表
	 * @return
	 */
	@RequestMapping("/showCart")
	public Result showCart(String userId,HttpServletRequest request, HttpServletResponse response) {
		try {
			if (StringUtils.isBlank(userId)) {
				//执行用户未登录状态下的业务逻辑
				return this.cookieCartService.showCart(request, response);
			}else {
				
				//执行用户登录状态下的业务逻辑
				return this.redisCartService.showCart(userId);
				
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return Result.error(null);
	}

查询购物车列表——遍历map集合,将map中的元素放入的list中

1.controller

完善if…else…中的else语句

代码语言:javascript复制
@RequestMapping("/deleteItemFromCart")
	public Result deleteItemFromCart(String userId, Long itemId, HttpServletRequest request,
			HttpServletResponse response) {

		try {
			if (StringUtils.isBlank(userId)) {
				// 执行用户未登录状态下的业务逻辑
				return this.cookieCartService.deleteItemFromCart(itemId, request, response);
			} else {
				// 执行用户登录状态下的业务逻辑
				return this.redisCartService.deleteItemFromCart(itemId, userId);

			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return Result.error(null);
	}

2.接口实现类

代码语言:javascript复制
/**
	 * 查看购物车(登陆状态下)
	 */
	@Override
	public Result showCart(String userId) {
		// 创建返回值对象
		List<CartItem> list = new ArrayList<CartItem>();

		// 从Redis中获取购物车对象
		Map<String, CartItem> cartMap = this.getCartToRedis(userId);

		// 遍历map集合的key,将map中的元素放入的list中
		Set<String> keys = cartMap.keySet();
		for (String key : keys) {
			list.add(cartMap.get(key));
		}

		return Result.ok(list);
	}

修改购物车中商品的数量——操作Redis

1.controller

代码语言:javascript复制
	@RequestMapping("/updateItemNum")
	public Result updateItemNum(String userId, Long itemId, @RequestParam(defaultValue = "1") Integer num,
			HttpServletRequest request, HttpServletResponse response) {

		try {
			if (StringUtils.isBlank(userId)) {
				// 执行用户未登录状态下的业务逻辑
				return this.cookieCartService.updateItemNum(itemId, num, request, response);
			} else {
				// 执行用户登录状态下的业务逻辑
				return this.redisCartService.updateItemNum(itemId, num, userId);

			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return Result.error(null);
	}

2.接口类

代码语言:javascript复制
	/**
	 * 修改购物车商品数量
	 */
	@Override
	public Result updateItemNum(Long itemId, Integer num, String userId) {
		// 从Redis中获取购物车对象
		Map<String, CartItem> cartMap = this.getCartToRedis(userId);
		CartItem carItem = cartMap.get(userId.toString());
		if (cartMap != null) {
			carItem.setNum(num);
		}
		// 将新的购物车缓存到redis
		this.addCartToRedis(userId, cartMap);

		return Result.ok();
	}

删除购物车中商品——操作Redis

1.controller

代码语言:javascript复制
	@RequestMapping("/deleteItemFromCart")
	public Result deleteItemFromCart(String userId, Long itemId, HttpServletRequest request,
			HttpServletResponse response) {

		try {
			if (StringUtils.isBlank(userId)) {
				// 执行用户未登录状态下的业务逻辑
				return this.cookieCartService.deleteItemFromCart(itemId, request, response);
			} else {
				// 执行用户登录状态下的业务逻辑
				return this.redisCartService.deleteItemFromCart(itemId, userId);

			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return Result.error(null);
	}

2.接口类

代码语言:javascript复制
	/**
	 * 删除购物车中的商品
	 */
	@Override
	public Result deleteItemFromCart(Long itemId, String userId) {
		// 从Redis中获取购物车
		Map<String, CartItem> cartMap = this.getCartToRedis(userId);
		cartMap.remove(itemId.toString());
		this.addCartToRedis(userId, cartMap);
		// 将新的购物车缓存到redis
		return Result.ok();
	}

七、用户注册与登录服务——单点登录(SSO)功能实现

介绍

单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他联邦系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,

这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统

这种方式减少了由登录产生的时间消耗,辅助了用户管理,是目前比较流行的。

环境搭建

1.创建项目.修改pom文件

Eureka,Feign,LCN,Mapper(pojo),utils,打包插件

代码语言:javascript复制
<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>
  <parent>
    <groupId>ah.szxy.project</groupId>
    <artifactId>bz_parent</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <artifactId>frontend_sso</artifactId>
	
	<dependencies>
		<!--Spring Boot Web Starter-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!--Spring Cloud Eureka Client Starter-->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
		<!--Spring Cloud OpenFeign Starter -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-openfeign</artifactId>
		</dependency>
		<!-- lcn client -->
		<dependency>
			<groupId>ah.szxy.project</groupId>
			<artifactId>common_tx_manager_client</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>
		
		<!-- Mapper -->
		<dependency>
			<groupId>ah.szxy.project</groupId>
			<artifactId>common_mapper</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>
		<!-- Utils -->
		<dependency>
			<groupId>ah.szxy.project</groupId>
			<artifactId>common_utils</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>
	</dependencies>
	
	<!-- 打包插件,打成jar,用于在虚拟机中发布服务 -->
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

2.创建全局配置文件

代码语言:javascript复制
spring:       #配置应用名,数据库连接参数
  application:
    name: frontend-sso
  datasource:
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/bz_shop?useSSL=false
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource
  redis:
    host: 192.168.179.131
    port: 6379
    
server:       #配置端口号
  port: 9090

eureka:       #配置Eureka服务注册中心地址
  client:
    serviceUrl:
      defaultZone: http://eureka-server:8761/eureka/

tx-lcn: 
  client:
      manager-address: 192.168.179.138:8070    #TM服务端事务消息端口

3.启动类

开启Eureka发现,Feign客户端,Mapper扫描

代码语言:javascript复制
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("ah.szxy.mapper")
public class FrontendSsoApplication {
	public static void main(String[] args) {
		SpringApplication.run(FrontendSsoApplication.class, args);
	}
}

注册功能实现

注册校验接口文档

注册接口文档

上游服务

不需要操作Redis,故只有上游服务

1.controller(根据接口文档书写)

代码语言:javascript复制
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import ah.szxy.frontend.sso.service.SSOService;
import ah.szxy.pojo.TbUser;
import ah.szxy.utils.Result;

@RestController
@RequestMapping("/sso")
public class SSOController {
	
	@Autowired
	private SSOService ssoService;
	/**
	 * 用户注册校验
	 * @param checkValue
	 * @param checkFlag
	 * @return
	 */
	@RequestMapping("/checkUserInfo/{checkValue}/{checkFlag}")
	public Result checkUserInfo(@PathVariable String checkValue,@PathVariable Integer checkFlag) {
		
		try {
			return this.ssoService.checkUserInfo(checkValue, checkFlag);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return Result.error(null);
	}
	
	/**
	 * 用户登陆失败
	 * @param tbUser
	 * @return
	 */
	@RequestMapping("/userRegister")
	public Result userRegister(TbUser tbUser) {
		
		try {
			return this.ssoService.userRegister(tbUser);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return Result.error(null);
	}
}

2.接口类

代码语言:javascript复制
public interface SSOService {
	/**
	 * 用户注册校验
	 * @param checkValue
	 * @param checkFlag
	 * @return
	 */
	Result checkUserInfo(String checkValue,Integer checkFlag);
	/**
	 * 用户注册
	 * @param tbUser
	 * @return
	 */
	Result userRegister(TbUser tbUser);
}

3.接口实现类

注意: 在注册时(相当于添加数据),需要使用MD5进行不可逆的加密,并且补齐表单没有的数据

在数据库保存的是加密后的数据, 进行验证时,对密码直接再使用一次MD5加密,然后将加密后结果与数据库中的数据比对,如果一样说明密码正确

代码语言:javascript复制
import java.util.Date;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import ah.szxy.frontend.sso.service.SSOService;
import ah.szxy.mapper.TbUserMapper;
import ah.szxy.pojo.TbUser;
import ah.szxy.pojo.TbUserExample;
import ah.szxy.pojo.TbUserExample.Criteria;
import ah.szxy.utils.MD5Utils;
import ah.szxy.utils.Result;

@Service
public class SSOServiceImpl implements SSOService {
	
	@Autowired
	private TbUserMapper tbUserMapper;
	
	/**
	 * 用户注册校验
	 */
	@Override
	public Result checkUserInfo(String checkValue, Integer checkFlag) {
		TbUserExample example=new TbUserExample();
		Criteria c = example.createCriteria();
		
		//根据checkFlag判断时校验用户名还是手机号, 1:用户名,2手机号
		if (checkFlag==1) {
			c.andUsernameEqualTo(checkValue);
		}else if (checkFlag==2) {
			c.andPhoneEqualTo(checkValue);
		}
		
		//查询符合条件的个数,如果不为0说明重复
		int result = this.tbUserMapper.countByExample(example);
		if (result!=0) {
			return Result.error(null);
		}else {
			return Result.ok(checkValue);
		}
	}
	
	/**
	 * 用户注册(插入操作)
	 */
	@Override
	public Result userRegister(TbUser tbUser) {
		
		//将密码使用MD5进行加密
		String pwd=MD5Utils.digest(tbUser.getPassword());
		tbUser.setPassword(pwd);
		//插入必做-----补齐数据
		Date date=new Date();
		tbUser.setCreated(date);
		tbUser.setUpdated(date);
		
		this.tbUserMapper.insert(tbUser);
		return Result.ok();
	}

}

4.MD5加密的工具类

代码语言:javascript复制
package ah.szxy.utils;

import java.security.MessageDigest;

/**
 * MD5加密工具
 */
public class MD5Utils {
	
	public static String digest(String source){
		try{
			MessageDigest digest = MessageDigest.getInstance("MD5");
			
			byte[] temp = digest.digest(source.getBytes());
			
			StringBuilder builder = new StringBuilder("");
			
			for(byte b : temp){
				String s = Integer.toHexString(0xFF & b);
				if(s.length() == 1){
					s = "0"   s;
				}
				builder.append(s);
			}
			
			return builder.toString();
		}catch(Exception e){
			return null;
		}
	}

}

用户登陆功能的实现

思路:

1.根据用户名密码查询数据库

2.判断用户是否存在. 如果用户存在,将userToken添加到 Redis中(他是利用UUID生成的一个字符串,用于唯一标识用户的信息)

3.调用添加userToken到redis的服务,判断是否添加成功

下游服务(操作Redis)

用户登陆实质 : 将用户的userToken信息存到Redis中

用户退出实质: 将用户的userToken信息从Redis中移除

1.接口类

代码语言:javascript复制
	/**
	 * 将用户添加到缓存
	 * @param tbUser
	 * @param token
	 */
	void userLogin(TbUser tbUser,String userToken);
	/**
	 * 用户退出
	 * @param userToken
	 */
	void userLogout(String userToken);

2.实现类

代码语言:javascript复制
#全局配置文件中
#用户信息的缓存
user_session_redis_key: user:session:redis:key
代码语言:javascript复制
@Service
public class SSOServiceImpl implements SSOService {
	
	@Autowired
	private RedisTemplate<String, Object>redisTemplate;
	@Value("${user_session_redis_key}")
	private String user_session_redis_key;
	
	/**
	 * 用户登陆
	 */
	@Override
	public void userLogin(TbUser tbUser, String userToken) {
		//出于安全考虑,将密码设置空
		tbUser.setPassword("");
		
		//添加缓存,设置key(固定字符 token),设置value,设置超时时间,设置超时时间单位
		this.redisTemplate.opsForValue().set(this.user_session_redis_key ":" userToken, tbUser, 1, TimeUnit.DAYS);
	}

	/**
	 * 用户退出
	 */
	@Override
	public void userLogout(String userToken) {
		this.redisTemplate.delete(this.user_session_redis_key ":" userToken);
	}

}

3.controller

参数列表中存在单个参数时,建议使用@RequestParam进行参数矫正,为对象时使用@RequestBody,主要用来接收前端传递给后端的json字符串中的数据的

代码语言:javascript复制
@RestController
@RequestMapping("/redis/sso")     
public class SSOController {
	@Autowired
	private SSOService ssoService;
	/**
	 * 将用户信息添加进缓存
	 * @param tbUser
	 * @param token
	 */
	@RequestMapping("/userLogin")
	public void userLogin(@RequestBody TbUser tbUser,@RequestParam(value="userToken") String userToken) {
		this.ssoService.userLogin(tbUser, userToken);
	}
	
	@RequestMapping("/logout")
	public void userLogout(@RequestParam(value="userToken") String userToken) {
		this.ssoService.userLogout(userToken);
	}
}
上游服务

1.controller

注意对接前端接口, 在用户退出的时候,前端发送的是token,所以需要参数矫正

否则出现: status 400 reading CommonRedisFeignClient#userLogout(String)

代码语言:javascript复制
	/**
	 * 用户登陆
	 * @param username
	 * @param password
	 * @return
	 */
	@RequestMapping("/userLogin")
	public Result userLogin(String username,String password) {
		
		try {
			return this.ssoService.userLogin(username, password);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return Result.error(null);
	}
	/**
	 * 登陆退出
	 * @param userToken
	 * 
	 * 注意:前端表单发送的是token,而不是userToken,所以需要进行参数矫正
	 *  否则出现: status 400 reading CommonRedisFeignClient#userLogout(String)
	 * @return
	 */
	@RequestMapping("/logOut")
	public Result logOut(@RequestParam(value="token")String userToken) {
		
		try {
			return this.ssoService.logOut(userToken);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return Result.error(null);
	}

2.Feign接口

代码语言:javascript复制
@FeignClient("common-redis")
public interface CommonRedisFeignClient {
	
	//----------------------------/redis/sso-------------------------------------------------
	@PostMapping("/redis/sso/userLogin")
	void userLogin(@RequestBody TbUser tbUser,@RequestParam(value="userToken") String userToken);
	
	@PostMapping("/redis/sso/logout")
	void userLogout(@RequestParam(value="userToken") String userToken);
}

3.接口类

代码语言:javascript复制
Result userRegister(TbUser tbUser);
	/**
	 * 用户登陆
	 * @param tbUser
	 * @return
	 */
	Result userLogin(String username,String password);
	/**
	 * 登陆退出
	 * @param userToken
	 * @return
	 */
	Result logOut(String userToken);

4.实现类

1.根据用户名密码查询数据库

2.判断用户是否存在. 如果用户存在,将userToken添加到 Redis中(他是利用UUID生成的一个字符串,用于唯一标识用户的信息)

3.调用添加userToken到redis的服务,判断是否添加成功

代码语言:javascript复制
	/**
	 * 用户登陆,并向Redis中添加userToken的缓存信息
	 */
	@Override
	public Result userLogin(String username, String password) {

		// 1.根据用户名密码查询数据库
		TbUser tbUser = this.login(username, password);

		// 2.判断用户是否存在. 如果用户存在,将userToken添 加 到 Redis中
		if (tbUser != null) {
			// 生成userToken
			String userToken = UUID.randomUUID().toString();
			// 3.调用添加userToken到redis的服务,判断是否添加成功
			Integer flag = this.insertUserToRedis(tbUser, userToken);
			if (flag == 200) {
				// 如果添加缓存成功,返回这些信息
				Map<String, String> map = new HashMap<String, String>();
				map.put("token", userToken);
				map.put("userId", tbUser.getId().toString());
				map.put("username", tbUser.getUsername());
				return Result.ok(map);

			} else if (flag == 500) {
				// 如果添加缓存失败,提示登陆失败
				return Result.error("登陆失败");
			}

		}
		return Result.error("用户名或密码有误,请重新输入");
	}

	/**
	 * 封装添用户信息加缓存到redis的方法
	 * 
	 * @param tbUser
	 * @param userToken 作为用户的key
	 * @return
	 */
	private Integer insertUserToRedis(TbUser tbUser, String userToken) {
		try {
			// 对于在redis插入缓存需要捕获异常,以免影响下面代码的执行
			this.commonRedisFeignClient.userLogin(tbUser, userToken);
			return 200;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return 500;
	}

	/**
	 * 封装用户登陆的方法
	 * @param username
	 * @param password
	 */
	private TbUser login(String username, String password) {
		//因为在注册时被加密,所以在登陆时,也需要通过加密后的字符串查询
		String pwd = MD5Utils.digest(password);

		
		//查询是否存在
		TbUserExample example=new TbUserExample();
		Criteria c = example.createCriteria();
		c.andUsernameEqualTo(username);
		c.andPasswordEqualTo(pwd);
		List<TbUser> list = this.tbUserMapper.selectByExample(example);
		if (!list.isEmpty()) {//对查询到的结果需要判空
			return list.get(0);
		}
		return null;
	}
	
	/**
	 * 登陆退出
	 */
	@Override
	public Result logOut(String userToken) {
		
		this.commonRedisFeignClient.userLogout(userToken);
		return Result.ok();
	}

测试

用户登陆成功后,redis会显示缓存信息

用户退出后 ,这些缓存信息会被删除

拦截器的使用-去结账时检测用户是否登录(Cookie和Redis)

拦截器是我们项目开发的重要一环, 保准数据的安全性和准确性和完整性

在校验时,不仅需要去Cookie检查用户的token是否存在,而且需要去Redis检查token是否存在

那为什么要同时检验Cookie和Redis中的token呢?

因为他们都是有有效期的, 有可能因为一方的有效期过期而导致服务器端(Redis)或客户端(Cookie)中的token已被移除,但是仍能够进行现金操作,会带来相关的安全问题.

这时候就需要有拦截器的出现 ,任何一方没有token,拦截它并让他重新登陆

下游服务Common-Redis(检测token是否存在)

1.SSOController中

代码语言:javascript复制
	/**
	 * 结算时,检测用户是否登录
	 * 根据用户token 校验用户在redis 中是否失效
	 */
	@RequestMapping("/checkUserToken")
	public TbUser checkUserToken(@RequestParam String token) {
		return this.ssoService.checkUserToken(token);
	}

2.在SSOService添加抽象方法

代码语言:javascript复制
/**
	 * 结算时,检查用户是否登录
	 * @param token
	 * @return
	 */
	TbUser checkUserToken(String token);

3.在实现类重写抽象方法

代码语言:javascript复制
/**
	 * 结算时,检测用户是否登录
	 * 首先去检查是否有token
	 * 然后检查是否有缓存
	 */
	@Override
	public TbUser checkUserToken(String token) {
		
		return (TbUser) this.redisTemplate.opsForValue().get(this.user_session_redis_key ":" token);
	}
上游服务frontend_cart

1.CommonRedisFeignClient声明需要调用的下游服务方法的接口

代码语言:javascript复制
	@PostMapping("/redis/sso/checkUserToken")
	TbUser checkUserToken(@RequestParam String token);

2.接口类

代码语言:javascript复制
/**
 * 校验用户登录业务层接口
 * @author chy
 *
 */
public interface UserCheckService {
	/**
	 * 校验用户登录(通过查询Redis中)
	 * @param token
	 * @return
	 */
	TbUser checkUserToken(String token);
}

3.实现类

代码语言:javascript复制
@Service
public class UserCheckServiceImpl implements UserCheckService {
	
	@Autowired
	private CommonRedisFeignClient commonRedisFeignClient;
	
	/**
	 * 校验用户登录(通过查询Redis中)
	 */
	@Override
	public TbUser checkUserToken(String token) {
		return this.commonRedisFeignClient.checkUserToken(token);
	}

}

4.UserLoginInterceptor拦截器类

a.对用户的token 做判断(通过Cookie),通过HttpServletRequest 对象获取

b.如果用户token 不为空,则校验用户在redis 中是否失效(调用下游服务,通过一个固定长字符串加上token作为key来查询value,也就是用户信息tbUser)

为何查询redis调用的参数也是从Cookie中查询的token呢?

可以这样想,如果从服务端token如果都查不到token,那么肯定直接拦截到登陆页面了,

根本不需要进行下一步,只有第一步能够查到的情况下才会进行第二次查询Redis中的token

代码语言:javascript复制
package ah.szxy.frontend.cart.intercepter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import ah.szxy.frontend.cart.service.UserCheckService;
import ah.szxy.pojo.TbUser;

/**
 * 在结算之前判断用户是否登录的拦截器
 * @author chy
 *
 */
@Component
public class UserLoginInterceptor implements HandlerInterceptor{
	
	@Autowired
	private UserCheckService userCheckService;
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		
		//1.对用户的token 做判断(通过Cookie)
		String token = request.getParameter("token");
		if(StringUtils.isBlank(token)){
		return false;
		}
		
		//2.如果用户token 不为空,则校验用户在redis 中是否失效
		TbUser tbUser = this.userCheckService.checkUserToken(token);
		if(tbUser == null){
		return false;
		}
		return true;//放行
	}
	
		
	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		// TODO Auto-generated method stub
		HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
	}

	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception {
		// TODO Auto-generated method stub
		HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
	}
		
}

5.拦截器的配置类

代码语言:javascript复制
package ah.szxy.frontend.cart.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import ah.szxy.frontend.cart.intercepter.UserLoginInterceptor;

/**
 * 配置拦截器类
 * 
 * 相当于在SpringMVC中配置xml文件
 * 配置拦截器拦截的路径
 * @author chy
 *
 */
@Configuration
public class WebApplication implements WebMvcConfigurer{
	
	//注入拦截器
	@Autowired
	private UserLoginInterceptor userLoginInterceptor;
	
	
	//这里无需添加@Bean注解,因为它实现了WebMvcConfigurer接口,会读取被复写的方法
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		InterceptorRegistration registration =registry.addInterceptor(this.userLoginInterceptor);
		
		//拦截那个URI路径
		registration.addPathPatterns("/cart/goSettlement/**");
	}
	
}

八、开发百战商城前台系统

服务网关设置与超时调优

相关技术介绍

1.服务网关技术配置介绍

2.Hystrix技术解决超时问题介绍

服务网关整合Hystrix环境搭建

1.创建项目,修改pom文件

代码语言:javascript复制
<dependencies>
		<!-- web启动器 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!-- Eureka客户端 -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
		<!-- 网关服务 -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
		</dependency>
	</dependencies>
	<!-- 打包插件 -->
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

2.修改全局配置文件 application.yml

7070是网关服务的端口号(可自定义),

通过访问http://localhost:7070/即可访问到网关服务,根据定义的路由规则访问到对应服务

配置网关请求服务的超时时间, 解决使用网关后, 其他配置无误后页面超时导致的访问失败的问题

代码语言:javascript复制
spring:       #配置应用名,数据库连接参数
  application:
    name: common-zuul
    
server:       #配置端口号
  port: 7070
  
eureka:       #配置Eureka服务注册中心地址
  instance:     #使用真实ip,而不使用域名
    prefer-ip-address: true
  client:
    serviceUrl:
      defaultZone: http://eureka-server:8761/eureka/
      
zuul:
  sensitive-headers: true #全局配置,解决在网关服务中不传递请求头的问题(Cookie)
  routes:
    #后台商品服务路由规则   
    backend_item:    #服务名之间的连接号最好使用连接号"-"而不是下划线"_",该种配置是简化配置,这一行必须是服务名
      path: /backend_item/**
    #后台CMS 服务的路由规则
    backend_content:
      path: /backend_content/**
      
    #前台首页服务的路由规则
    frontend_portal:
      path: /frontend_potal/**
    #前台搜索服务的路由规则
    frontend-search:
      path: /frontend_search/**
    #前台用户注册与登录服务
    frontend-sso:
      path: /frontend_sso/**
    #前台订单服务
    frontend-order:
      path: /frontend_order/**
    #前台购物车服务
    frontend-cart:
      path: /frontend_cart/**
      
#配置网关请求服务的超时时间
#第一层hystrix 的超时时间
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 5000 #默认为线程池隔离,默认超时时间为1000ms
            
#第二层Ribbon 的超时时间设置
ribbon:
  ConnectTimeout: 3000 #设置请求连接的超时时间默认为5 秒
  ReadTimeout: 3000 #设置请求处理超时时间默认为5 秒

3.启动类

代码语言:javascript复制
/**
 * 网关服务
 * @author chy
 *
 */
@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
public class CommonZuulApplicatio {
	public static void main(String[] args) {
		
		SpringApplication.run(CommonZuulApplicatio.class, args);
	}
}

4.需要我们将以前服务的端口指向网关服务的端口

6.需要我们匹配路由规则的path

7.测试各项功能能否正常运行

注意: 配置网关后 ,需要首先启动网关服务再启动下游服务然后是上游服务

在网关中实现对服务降级处理

返回托底数据, 并在控制台打印, 需要为每个项目都配置一个这样的托底类undefined

代码语言:javascript复制
package ah.szxy.common.zuul.fallback;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;

import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;

/**
 * 对getRoute中的方法返回值中的项目进行降级处理,返回托底数据
 * 
 * 1.创建托底类,实现FallbackProvider接口 
 * 2.返回一个ClientHttpResponse对象
 * 效果:会在服务超时后,返回托底数据信息
 * @author chy
 *
 */
@Component
public class BackendContentFallback implements FallbackProvider {

	@Override
	public String getRoute() {
		return "backend_content";
	}

	@Override
	public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
		if (cause != null && cause.getCause() != null) {
			String c = cause.getCause().getMessage();
			System.out.println(route   "t"   c);
		}
		return new ClientHttpResponse() {

			@Override
			public HttpHeaders getHeaders() {
				HttpHeaders headers = new HttpHeaders();
				MediaType mediaType = new MediaType("application", "json", Charset.forName("utf-8"));
				headers.setContentType(mediaType);
				return headers;
			}

			@Override
			public InputStream getBody() throws IOException {
				String content = "服务超时,请重试";
				return new ByteArrayInputStream(content.getBytes());
			}

			@Override
			public String getStatusText() throws IOException {
				return this.getStatusCode().getReasonPhrase();
			}

			@Override
			public HttpStatus getStatusCode() throws IOException {
				return HttpStatus.OK;
			}

			@Override
			public int getRawStatusCode() throws IOException {
				return this.getStatusCode().value();
			}

			@Override
			public void close() {

			}
		};

	}

}

使用令牌桶算法实现限流

Google令牌桶实现限流原理
相关代码

返回状态码, 并在控制台打印相关数据, 表示限流器生效 RateLimiter.create(1)1:是每秒生成令牌的数量, 但是数量可以自定义 设置后,项目只支持我们每秒发送一次请求

代码语言:javascript复制
package ah.szxy.common.zuul.filter;

import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import com.google.common.util.concurrent.RateLimiter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;

/**
 * 限流器
 * 
 * @author chy
 *
 */
@Component
public class RateLimitFilter extends ZuulFilter {

	// 创建令牌桶(import com.google.common.util.concurrent.RateLimiter;)
	// RateLimiter.create(1)1:是每秒生成令牌的数量
	// 数值越大代表处理请求量月多,数值越小代表处理请求量越少
	private static final RateLimiter RATE_LIMIT = RateLimiter.create(1);

	@Override
	public Object run() throws ZuulException {
		// 是否能从令牌桶中获取到令牌,如果不能,返回一个403的状态码
		if (!RATE_LIMIT.tryAcquire()) {
			RequestContext requestContext = RequestContext.getCurrentContext();
			requestContext.setSendZuulResponse(false);
			requestContext.setResponseStatusCode(429);
			System.out.println("限流");// 控制台输出限流字样,表示令牌桶限流失效
		}
		return null;
	}

	@Override
	public boolean shouldFilter() {
		return true;
	}

	/**
	 * 限流器的优先级应为最高
	 * 
	 * @return
	 */
	@Override
	public int filterOrder() {
		return FilterConstants.SERVLET_DETECTION_FILTER_ORDER - 1;
	}

	@Override
	public String filterType() {
		return FilterConstants.PRE_TYPE;
	}

}

使用Postman 测试服务降级与网关限流

限流
查看托底数据

九、在项目中配置分布式配置中心

分布式配置中心介绍传送门

在项目中配置分布式配置中心的服务端

配置分布式配置中心的服务端,并且实现手动刷新

0.安装RabbitMQ(因为使用了RabbitMQ作为消息总线)

1.创建项目,修改pom文件

代码语言:javascript复制
	<dependencies>
		<!--Spring Boot Web Starter -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!--Spring Cloud Eureka Client Starter -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
		<!--Spring Cloud Config Server -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-config-server</artifactId>
		</dependency>
		<!--Spring Cloud AMQP: (RabbitMQ) Starter -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-bus-amqp</artifactId>
		</dependency>
	</dependencies>

	<!-- 打包插件,打成jar,用于在虚拟机中发布服务 -->
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

2.修改全局配置文件application.yml

代码语言:javascript复制
spring:       #配置应用名,数据库连接参数
  application:
    name: common-config
  cloud:      #配置配置中心服务端地址
    config:
      server:
        git:
          uri: https://gitee.com/TimePause/bzshop_config
    bus:
      refresh:
        enabled: true #开启自动刷新
  rabbitmq:     #配置RabbitMQ连接参数(在分布式配置中心服务端中添加消息总线)
    host: 192.168.179.136
    port: 5672
    username: mquser
    password: mquser  
    virtual-host: /  
    listener:
      simple:
        retry:
          enabled: true #开启重试
          max-attempts: 5  #重试次数

    
    
    
server:       #配置端口号
  port: 9000
  
eureka:       #配置Eureka服务注册中心地址
  instance:     #使用真实ip,而不使用域名
    prefer-ip-address: true
  client:
    serviceUrl:
      defaultZone: http://eureka-server:8761/eureka/
      
management:
  endpoints:
    web:
      exposure:
        include: bus-refresh  #在Greewitch.SR2 版中需要开启

3.创建启动类

@EnableConfigServer开启配置中心的注解

代码语言:javascript复制
package ah.szxy.common.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableDiscoveryClient
@EnableConfigServer
public class CommonConfigApplication {
	
	public static void main(String[] args) {
		SpringApplication.run(CommonConfigApplication.class, args);
	}
}

4.登录gitee, 在gitee 中创建远程仓库

在项目中配置分布式配置中心的客户端

1.添加如下坐标

代码语言:javascript复制
<!--Spring Cloud Config Client Starter -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-config</artifactId>
		</dependency>
		<!--Spring Cloud AMQP: (RabbitMQ) Starter -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-bus-amqp</artifactId>
		</dependency>

2.在每个客户端服务的配置文件application.yml中添加如下配置

代码语言:javascript复制
spring:
  rabbitmq:     #配置RabbitMQ连接参数(在分布式配置中心客户端中添加消息总线)
    host: 192.168.179.136
    port: 5672
    username: mquser
    password: mquser  
    virtual-host: /  
    listener:
      simple:
        retry:
          enabled: true #开启重试
          max-attempts: 5  #重试次数   

3.将配置文件application.yml修改名称后(-dev)上传到gitee 远程仓库中,目标分支master

上传所有项目, 除了配置中心服务端以及Eureka服务端,并删除本地项目的配置文件

4.在每个客户端服务中创建bootstrap.yml 文件

代码语言:javascript复制
spring:
  cloud:
    config:
      discovery:
        enabled: true
        service-id: common-config #指定配置中心服务端的服务名称
      name: frontend-cart #对应的{application}部分
      profile: dev #对应的{profile}部分
      uri: http://127.0.0.1:9000 #配置中心的具体地址,Greenwich 版中需要添加
      label: master

eureka:       #配置Eureka服务注册中心地址
  instance:     #使用真实ip,而不使用域名
    prefer-ip-address: true
  client:
    serviceUrl:
      defaultZone: http://eureka-server:8761/eureka/

5.测试分布式配置中心

注意: 配置分布式配置中心 ,需要首先启动配置中心服务再启动网关服务然后是下游服务和上游服务

6.测试自动刷新配置信息curl -X POST http://localhost:9000/actuator/bus-refresh

十、通过Hystrix 对下游服务做降级处理

返回托底数据实现步骤

  • 1.创建托底类,继承FallbackFactory接口,参数类型为Feign接口类CommonItemFeignClient
  • 2.实现相关方法,并在返回值中实例化Feign接口类,然后自动实例化它的接口
  • 3.修改feign的接口类,在@FeignClient注解中添加fallbackFactory,指定这个类并且以.class结尾
  • 4.在下游服务中在返回结果前,对需要事务操作(增删改)的数据进行判断,如果为空,手动抛出

托底类

代码语言:javascript复制
package ah.szxy.backend.item.fallback;

import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Component;

import ah.szxy.backend.item.feign.CommonItemFeignClient;
import ah.szxy.pojo.TbItem;
import ah.szxy.pojo.TbItemCat;
import ah.szxy.pojo.TbItemDesc;
import ah.szxy.pojo.TbItemParam;
import ah.szxy.pojo.TbItemParamItem;
import ah.szxy.utils.PageResult;
import feign.hystrix.FallbackFactory;

/**
 * Common-item 服务返回托底数据
 * 
 * 返回托底数据实现步骤
 * 1.创建托底类,继承FallbackFactory接口,参数类型为Feign接口类CommonItemFeignClient
 * 2.实现相关方法,并在返回值中实例化Feign接口类,然后自动实例化它的接口
 * 3.修改feign的接口类,在@FeignClient注解中添加fallbackFactory,指定这个类并且以.class结尾
 * 4.在下游服务中在返回结果前,对需要事务操作(增删改)的数据进行判断,如果为空,手动抛出RuntimeException()查不需要
 * @author chy
 *
 */
@Component
public class CommonItemFeignClientFallbackFactory implements
		FallbackFactory<CommonItemFeignClient>{

	@Override
	public CommonItemFeignClient create(Throwable arg0) {

		return new CommonItemFeignClient() {
			
			@Override
			public Integer updateTbItemParamItem(TbItemParamItem tbItemParamItem) {
				// TODO Auto-generated method stub
				return null;
			}
			
			@Override
			public Integer updateItemDesc(TbItemDesc tbItemDesc) {
				// TODO Auto-generated method stub
				return null;
			}
			
			@Override
			public Integer updateItem(TbItem tbItem) {
				// TODO Auto-generated method stub
				return null;
			}
			
			@Override
			public PageResult selectTbItemAllByPage(Integer page, Integer rows) {
				// TODO Auto-generated method stub
				return null;
			}
			
			@Override
			public TbItemParam selectItemParamByItemCatId(Long itemCatId) {
				// TODO Auto-generated method stub
				return null;
			}
			
			@Override
			public PageResult selectItemParamAll(Integer page, Integer rows) {
				// TODO Auto-generated method stub
				return null;
			}
			
			@Override
			public List<TbItemCat> selectItemCategoryByParentId(Long id) {
				// TODO Auto-generated method stub
				return null;
			}
			
			@Override
			public Map<String, Object> preUpdate(Long itemId) {
				// TODO Auto-generated method stub
				return null;
			}
			
			@Override
			public Integer insertTbItemParamItem(TbItemParamItem tbItemParamItem) {
				// TODO Auto-generated method stub
				return null;
			}
			
			@Override
			public Integer insertTbItem(TbItem tbItem) {
				// TODO Auto-generated method stub
				return null;
			}
			
			@Override
			public Integer insertItemParam(TbItemParam tbItemParam) {
				// TODO Auto-generated method stub
				return null;
			}
			
			@Override
			public Integer insertItemDesc(TbItemDesc tbItemDesc) {
				// TODO Auto-generated method stub
				return null;
			}
			
			@Override
			public Integer deleteItemParamById(Long id) {
				// TODO Auto-generated method stub
				return null;
			}
			
			@Override
			public Integer deleteItemById(TbItem tbItem) {
				// TODO Auto-generated method stub
				return null;
			}
		};
	}

}

修改Feign

代码语言:txt复制
手动抛出异常,因为托底数据的存在会导致项目不会输错误信息,

但是分布式事务LCN需要检测异常的存在才能进行数据的回滚,所以在增删改时需要手动抛出异常

十一、分布式日志管理

分布式日志管理相关软件安装以及整合详细介绍

1访问Kibanahttp://192.168.70.140:5601/app/kibana

2.修改pom文件

代码语言:javascript复制
<!--Logback-->
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
</dependency>

3.添加日志配置文件logback.xml

代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
    <property name="LOG_HOME" value="${catalina.base}/logs/" />
    <!-- 控制台输出 -->
    <appender name="Stdout" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 日志输出编码 -->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
            </pattern>
        </layout>
    </appender>

    <!-- 为 logstash 输出的 JSON 格式的 Appender -->
    <appender name="logstash"  class="net.logstash.logback.appender.LogstashTcpSocketAppender">
        <destination>192.168.179.138:9250</destination> <!-- 日志输出编码 -->
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <timestamp>
                    <timeZone>UTC</timeZone>
                </timestamp>
                <pattern>
                    <pattern>
                        {"severity": "%level",
                        "service": "${springAppName:-}",
                        "trace": "%X{X-B3-TraceId:-}",
                        "span": "%X{X-B3-SpanId:-}",
                        "exportable": "%X{X-Span-Export:-}",
                        "pid": "${PID:-}",
                        "thread": "%thread",
                        "class": "%logger{40}",
                        "rest": "%message"
                        }
                    </pattern>
                </pattern>
            </providers>
        </encoder>
    </appender>

    <!-- 日志输出级别 -->
    <root level="INFO">
        <appender-ref ref="Stdout" />
        <appender-ref ref="logstash" />
    </root>

</configuration>

4.通过Kibana 查询日志

十二、 项目部署

1.给每个项目项目POM 文件中添加打包插件,并打包父项目

2.打包后,按照下面步骤执行打包教程

资料分享

链接:https://pan.baidu.com/s/1INJR7v4Ue77Xx6ATkLmBGw

提取码:zna4

0 人点赞