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断点, 然后再网页端发起请求
第一次进了缓存, 第二次命中缓存. 测试完成.


















