7. 好客租房-项目日常推进ing
本章节不涉及大量内容,主要是为了推荐项目代码日常进度而设置, 包括添加mock接口, 添加更新房源接口, 为系统添加缓存.
7.1 为前端系统提供mock服务
往往在项目开发中, 为实现前后端并行开发,后端需要对前端所有的请求都都进行支持。当然不要求立刻实现, 只需要先模拟数据返回以便前端测试连通性,页面美观.
mock数据不涉及微服务, 主要是在api-sereve模块中开发
7.1.1 新增文件 mock-data.properties
mock.indexMenu={"data":{"list":[{"id":1,"menu_name":"\u4E8C\u624B\u623F","menu_logo":"home","menu_path":"/home","menu_status":1,"menu_style":null},{"id":2,"menu_name":"\u65B0\u623F","menu_logo":null,"menu_path":null,"menu_status":null,"menu_style":null},{"id":3,"menu_name":"\u79DF\u623F","menu_logo":null,"menu_path":null,"menu_status":null,"menu_style":null},{"id":4,"menu_name":"\u6D77\u5916","menu_logo":null,"menu_path":null,"menu_status":null,"menu_style":null},{"id":5,"menu_name":"\u5730\u56FE\u627E\u623F","menu_logo":null,"menu_path":null,"menu_status":null,"menu_style":null},{"id":6,"menu_name":"\u67E5\u516C\u4EA4","menu_logo":null,"menu_path":null,"menu_status":null,"menu_style":null},{"id":7,"menu_name":"\u8BA1\u7B97\u5668","menu_logo":null,"menu_path":null,"menu_status":null,"menu_style":null},{"id":8,"menu_name":"\u95EE\u7B54","menu_logo":null,"menu_path":null,"menu_status":null,"menu_style":null}]},"meta":{"status":200,"msg":"\u6D4B\u8BD5\u6570\u636E"}}
mock.indexInfo={"data":{"list":[{"id":1,"info_title":"\u623F\u4F01\u534A\u5E74\u9500\u552E\u4E1A\u7EE9\u7EE7","info_thumb":null,"info_time":null,"info_content":null,"user_id":null,"info_status":null,"info_type":1},{"id":2,"info_title":"\u4E0A\u534A\u5E74\u571F\u5730\u5E02\u573A\u4E24\u91CD\u5929\uFF1A\u4E00\u7EBF\u964D\u6E29\u4E09\u56DB\u7EBF\u91CF\u4EF7\u9F50\u5347","info_thumb":null,"info_time":null,"info_content":null,"user_id":null,"info_status":null,"info_type":1}]},"meta":{"status":200,"msg":"\u6D4B\u8BD5\u6570\u636E"}}
mock.indexFaq={"data":{"list":[{"question_name":"\u5728\u5317\u4EAC\u4E70\u623F\uFF0C\u9700\u8981\u652F\u4ED8\u7684\u7A0E\u8D39\u6709\u54EA\u4E9B\uFF1F","question_tag":"\u5B66\u533A,\u6D77\u6DC0","answer_content":"\u5404\u79CD\u8D39\u7528","atime":33,"question_id":1,"qnum":2},{"question_name":"\u4E00\u822C\u9996\u4ED8\u4E4B\u540E\uFF0C\u8D37\u6B3E\u591A\u4E45\u53EF\u4EE5\u4E0B\u6765\uFF1F","question_tag":"\u5B66\u533A,\u660C\u5E73","answer_content":"\u5927\u69821\u4E2A\u6708","atime":22,"question_id":2,"qnum":2}]},"meta":{"status":200,"msg":"\u6D4B\u8BD5\u6570\u636E"}}
mock.indexHouse={"data":{"list":[{"id":1,"home_name":"\u5B89\u8D1E\u897F\u91CC123","home_price":"4511","home_desc":"72.32\u33A1/\u5357 \u5317/\u4F4E\u697C\u5C42","home_infos":null,"home_type":1,"home_tags":"\u6D77\u6DC0,\u660C\u5E73","home_address":null,"user_id":null,"home_status":null,"home_time":12,"group_id":1},{"id":8,"home_name":"\u5B89\u8D1E\u897F\u91CC \u4E09\u5BA4\u4E00\u5385","home_price":"4500","home_desc":"72.32\u33A1/\u5357 \u5317/\u4F4E\u697C\u5C42","home_infos":null,"home_type":1,"home_tags":"\u6D77\u6DC0","home_address":null,"user_id":null,"home_status":null,"home_time":23,"group_id":2},{"id":3,"home_name":"\u5B89\u8D1E\u897F\u91CC \u4E09\u5BA4\u4E00\u5385","home_price":"4220","home_desc":"72.32\u33A1/\u5357 \u5317/\u4F4E\u697C\u5C42","home_infos":null,"home_type":2,"home_tags":"\u6D77\u6DC0","home_address":null,"user_id":null,"home_status":null,"home_time":1,"group_id":1},{"id":4,"home_name":"\u5B89\u8D1E\u897F\u91CC \u4E09\u5BA4\u4E00\u5385","home_price":"4500","home_desc":"72.32\u33A1/\u5357 \u5317/\u4F4E\u697C\u5C42","home_infos":"4500","home_type":2,"home_tags":"\u6D77\u6DC0","home_address":"","user_id":null,"home_status":null,"home_time":12,"group_id":2},{"id":5,"home_name":"\u5B89\u8D1E\u897F\u91CC \u4E09\u5BA4\u4E00\u5385","home_price":"4522","home_desc":"72.32\u33A1/\u5357 \u5317/\u4F4E\u697C\u5C42","home_infos":null,"home_type":3,"home_tags":"\u6D77\u6DC0","home_address":null,"user_id":null,"home_status":null,"home_time":23,"group_id":1},{"id":6,"home_name":"\u5B89\u8D1E\u897F\u91CC \u4E09\u5BA4\u4E00\u5385","home_price":"4500","home_desc":"72.32\u33A1/\u5357 \u5317/\u4F4E\u697C\u5C42","home_infos":null,"home_type":3,"home_tags":"\u6D77\u6DC0","home_address":null,"user_id":null,"home_status":null,"home_time":1221,"group_id":2},{"id":9,"home_name":"\u5B89\u8D1E\u897F\u91CC \u4E09\u5BA4\u4E00\u5385","home_price":"4500","home_desc":"72.32\u33A1/\u5357 \u5317/\u4F4E\u697C\u5C42","home_infos":null,"home_type":4,"home_tags":"\u6D77\u6DC0","home_address":null,"user_id":null,"home_status":null,"home_time":23,"group_id":1}]},"meta":{"status":200,"msg":"\u6D4B\u8BD5\u6570\u636E"}}
mock.infosList1={"data":{"list":{"total":8,"data":[{"id":13,"info_title":"wwwwwwwwwwwww","info_thumb":null,"info_time":null,"info_content":null,"user_id":null,"info_status":null,"info_type":1},{"id":12,"info_title":"\u623F\u4F01\u534A\u5E74\u9500\u552E\u4E1A\u7EE9\u7EE7","info_thumb":null,"info_time":null,"info_content":null,"user_id":null,"info_status":null,"info_type":1}]}},"meta":{"status":200,"msg":"\u83B7\u53D6\u6570\u636E\u6210\u529F"}}
mock.infosList2={"data":{"list":{"total":4,"data":[{"id":9,"info_title":"\u623F\u4F01\u534A\u5E74\u9500\u552E\u4E1A\u7EE9\u7EE7\u7EED\u51B2\u9AD8\u4E09\u5DE8\u5934\u9500\u552E\u989D\u8FC7\u4EBF","info_thumb":null,"info_time":null,"info_content":null,"user_id":null,"info_status":null,"info_type":2},{"id":7,"info_title":"\u623F\u4F01\u534A\u5E74\u9500\u552E\u4E1A\u7EE9\u7EE7\u7EED\u51B2\u9AD8\u4E09\u5DE8\u5934\u9500\u552E\u989D\u8FC7\u4EBF","info_thumb":null,"info_time":null,"info_content":null,"user_id":null,"info_status":null,"info_type":2}]}},"meta":{"status":200,"msg":"\u83B7\u53D6\u6570\u636E\u6210\u529F"}}
mock.infosList3={"data":{"list":{"total":10,"data":[{"username":"tom","question_name":"\u5728\u5317\u4EAC\u4E70\u623F\uFF0C\u9700\u8981\u652F\u4ED8\u7684\u7A0E\u8D39\u6709\u54EA\u4E9B\uFF1F","question_tag":"\u5B66\u533A,\u6D77\u6DC0","answer_content":"\u5404\u79CD\u8D39\u7528","atime":33,"question_id":1,"qnum":2},{"username":"tom","question_name":"\u4E00\u822C\u9996\u4ED8\u4E4B\u540E\uFF0C\u8D37\u6B3E\u591A\u4E45\u53EF\u4EE5\u4E0B\u6765\uFF1F","question_tag":"\u5B66\u533A,\u660C\u5E73","answer_content":"\u5927\u69821\u4E2A\u6708","atime":22,"question_id":2,"qnum":2}]}},"meta":{"status":200,"msg":"\u83B7\u53D6\u6570\u636E\u6210\u529F"}}
mock.my={"data":{"id":1,"username":"tom","password":"123","mobile":"123","type":null,"status":null,"avatar":"public/icon.png"},"meta":{"status":200,"msg":"\u83B7\u53D6\u6570\u636E\u6210\u529F"}}
7.1.2 创建MockConfig
package cn.itcast.haoke.dubbo.api.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
/**
* 读取 mock-data.properties 中的内容
*
* @author 过道
*/
@Configuration
@PropertySource("classpath:mock-data.properties")
@ConfigurationProperties(prefix = "mock")
@Data
public class MockConfig {
private String indexMenu;
private String indexInfo;
private String indexFaq;
private String indexHouse;
private String infosList1;
private String infosList2;
private String infosList3;
private String my;
}
7.1.3 创建MockController
package cn.itcast.haoke.dubbo.api.controller;
import cn.itcast.haoke.dubbo.api.config.MockConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 模拟数据响应, 通过读取mock-data.properties的内容来返回假数据
*
* @author 过道
*/
@RequestMapping("mock")
@RestController
@CrossOrigin
public class MockController {
@Autowired
private MockConfig mockConfig;
/**
* 菜单
*/
@GetMapping("index/menu")
public String indexMenu() {
return this.mockConfig.getIndexMenu();
}
/**
* 首页资讯
*/
@GetMapping("index/info")
public String indexInfo() {
return this.mockConfig.getIndexInfo();
}
/**
* 首页问答
*/
@GetMapping("index/faq")
public String indexFaq() {
return this.mockConfig.getIndexFaq();
}
/**
* 首页房源信息
*/
@GetMapping("index/house")
public String indexHouse() {
return this.mockConfig.getIndexHouse();
}
/**
* 查询资讯
*/
@GetMapping("infos/list")
public String infosList(@RequestParam("type") Integer type) {
switch (type) {
case 1:
return this.mockConfig.getInfosList1();
case 2:
return this.mockConfig.getInfosList2();
case 3:
return this.mockConfig.getInfosList3();
}
return this.mockConfig.getInfosList1();
}
/**
* 我的中心
*/
@GetMapping("my/info")
public String myInfo() {
return this.mockConfig.getMy();
}
}
7.1.4 使用swagger-ui测试
启动ApiDubboApplication项目, 打开swagger, 随便找到一个mock接口, 点击测试. 表现正常.
7.2 实现后台系统的更新房源数据功能
这个完全无脑实现下就可以了. 再熟悉下相关文件分布就可以了. 主要是为了功能的完整性.
// Controller
@PutMapping
@ResponseBody
public ResponseEntity<Void> update(@RequestBody HouseResources houseResources) {
try {
boolean bool = this.houseResourcesService.update(houseResources);
if (bool) {
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
// service
public boolean update(HouseResources houseResources) {
return this.apiHouseResourcesService.updateHouseResources(houseResources);
}
// dubbo服务的service 接口
/**
* 修改房源
*
* @param houseResources
* @return
*/
boolean updateHouseResources(HouseResources houseResources);
// service接口的实现
@Override
public boolean updateHouseResources(HouseResources houseResources) {
return this.houseResourcesService.updateHouseResources(houseResources);
}
// houseResourcesService的实现.
@Override
public boolean updateHouseResources(HouseResources houseResources) {
return super.update(houseResources) == 1;
}
7.3 系统添加Redis缓存
在接口服务中,如果每一次都进行数据库查询,那么必然会给数据库造成很大的并发压力。所以需要为接口添加缓
存,缓存技术选用Redis,并且使用Redis的集群,Api使用Spring-Data-Redis。
这里省去了搭建Redis相关的环节(默认大家都会了哈), 如果需要请自行百度. 我这里使用的是已经搭建好的单机版本.
一个花费五秒钟的思考问题:
缓存应该加api处还是dubbo服务处?
7.3.1 引入依赖
揭晓答案
加在api处,可以减少对java系统的负载, 但是会有部分重复缓存的内容. dubbo服务处则依然会占用dubbo造成的带宽, 也不会降低 api模块的负载, 并且往往需要每个dubbo模块都维护自己的服务.
因为我们是小型项目(就一个后端), 我们这里选择加载api处, 最大程度的降低我们的任务量.
在api模块引入redis相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
7.3.2 编写Redis的配置文件
我直接卸载application.properties中
# redis集群配置
spring.redis.jedis.pool.max-wait = 5000
spring.redis.jedis.pool.max-Idle = 100
spring.redis.jedis.pool.min-Idle = 10
spring.redis.timeout = 1000
spring.redis.port=6379
# TODO 记得替换为你自己的IP, port一般都是6379, 如果你不是也需要替换哦
spring.redis.host=127.0.0.1
spring.redis.password=test123
7.3.3 编写配置类
package cn.itcast.haoke.dubbo.api.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* 本次不上分布式redis, 先使用单机版代替, 未来上分布式
*
* @author 过道
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory
redisConnectionfactory) {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionfactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
7.3.4 测试项目与Redis连通性
我们依然使用junit进行测试.
package cn.itcast.haoke.dubbo.api;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Set;
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestRedis {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Test
public void testSave() {
for (int i = 0; i < 100; i++) {
this.redisTemplate.opsForValue().set("key_" + i, "value_" + i);
}
Set<String> keys = this.redisTemplate.keys("key_*");
for (String key : keys) {
String value = this.redisTemplate.opsForValue().get(key);
System.out.println(value);
this.redisTemplate.delete(key);
}
}
}
7.3.5 添加缓存逻辑
在编写代码之前我简单介绍一下用到的技术
我们需要拦截所有发给本系统Controller的请求, 所以我们将用到拦截器, 拦截请求后判断是否已被缓存,如果已被缓存, 那么直接从缓存中取出结果.
如果没有被缓存, 那么走原有逻辑, 在发送响应之前, 我们需要将响应内容缓存到Redis中.
流程图如下:
首先, 使用Spring的拦截器拦截所有HTTP请求, 代码:
package cn.itcast.haoke.dubbo.api.interceptor;
import cn.itcast.haoke.dubbo.server.util.IOUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
@Component
public class RedisCacheInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static ObjectMapper mapper = new ObjectMapper();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler) throws Exception {
if (!StringUtils.equalsIgnoreCase(request.getMethod(), "get")) {
// 非get请求,如果不是graphql请求,放行
if (!StringUtils.equalsIgnoreCase(request.getRequestURI(), "/graphql")) {
return true;
}
}
String data =
this.redisTemplate.opsForValue().get(createRedisKey(request));
if (StringUtils.isEmpty(data)) {
// 缓存未命中
return true;
}
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
// 支持CORS跨域
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods",
"GET,POST,PUT,DELETE,OPTIONS");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Token");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.getWriter().write(data);
return false;
}
public static String createRedisKey(HttpServletRequest request) throws
Exception {
String paramStr = request.getRequestURI();
Map<String, String[]> parameterMap = request.getParameterMap();
if (parameterMap.isEmpty()) {
paramStr += IOUtils.toString(request.getInputStream(), "UTF-8");
} else {
paramStr += mapper.writeValueAsString(request.getParameterMap());
}
String redisKey = "WEB_DATA_" + paramStr;
return redisKey;
}
}
然后我们需要注册拦截器到Spring.
package cn.itcast.haoke.dubbo.api.config;
import cn.itcast.haoke.dubbo.api.interceptor.RedisCacheInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private RedisCacheInterceptor redisCacheInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(this.redisCacheInterceptor).addPathPatterns("/**");
}
}
最后我们将查询请求的响应内容进行一个缓存. 这里使用的技术也是Spring提供的@ControllerAdvice注解.
package cn.itcast.haoke.dubbo.api.interceptor;
import cn.itcast.haoke.dubbo.api.controller.GraphQLController;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.time.Duration;
/**
* ControllerAdvice是Spring提供的, 会在结果被发送前进行拦截, 拦截逻辑自己实现, 这样就可以实现缓存了.
*/
@ControllerAdvice
public class MyResponseBodyAdvice implements ResponseBodyAdvice {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private ObjectMapper mapper = new ObjectMapper();
/**
* 对于给定的controller里的方法, 是否要进行拦截的一个判断.
* @param returnType 返回类型
* @param converterType 转化器
* @return 是否可以执行 'beforeBodyWrite'方法.
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
// 如果方法上有GetMapping, 那么触发缓存, 这里的底层逻辑是GetMapping按照RestFul来说就是查询.
if (returnType.hasMethodAnnotation(GetMapping.class)) {
return true;
}
// 如果 是Post,那么只缓存 GraphQLController 的返回内容, 因为 GraphQL 也是对查询结果的一种缓存
if (returnType.hasMethodAnnotation(PostMapping.class) &&
StringUtils.equals(GraphQLController.class.getName(),
returnType.getExecutable().getDeclaringClass().getName())) {
return true;
}
return false;
}
/**
* 在返回给前端之前会调用这个方法, 将返回的内容提前缓存起来
* @param body
* @param returnType
* @param selectedContentType
* @param selectedConverterType
* @param request
* @param response
* @return
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest
request, ServerHttpResponse response) {
try {
String redisKey =
RedisCacheInterceptor.createRedisKey(((ServletServerHttpRequest)
request).getServletRequest());
String redisValue;
if (body instanceof String) {
redisValue = (String) body;
} else {
redisValue = mapper.writeValueAsString(body);
}
this.redisTemplate.opsForValue().set(redisKey, redisValue,
Duration.ofHours(1));
} catch (Exception e) {
e.printStackTrace();
}
return body;
}
}
7.3.6 使用swagger或者插件进行debug测试
在拦截器 RedisCacheInterceptor 类上加上debug断点, 然后再网页端发起请求
第一次进了缓存, 第二次命中缓存. 测试完成.