项目基本架构搭建
以下过程,我们以idea为开发工具,新建一个springboot项目。
开发环境:
- idea
- jdk 1.8
- maven 3.3.9
- mysql 5.7
1. 新建springboot项目
打开idea,新建一个project,选择Spring Initializr。project基本信息填写如下:
接下来,我们来选择一下我们需要集成的框架或中间件,就我们现在刚开发阶段,我们选择以下依赖就可以了:
- web
- Lombok
- Devtools
- freemaker
- Mysql
- Redis
当然了,有些依稀需要我们去完成一些配置,比如我们的mysql、redis需要配置连接信息,一般来说springboot有帮我们完成了很多默认配置,只要按照默认配置来,我们都不需要去修改或者写配置,比如freemaker的~
选择好了之后,idea会自动我们添加依赖,打开项目如下:
pom.xml的依赖包如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
现在,我们去一步步完成框架的集成。
2. 集成lombok
刚才我们已经自动导入了lombok的依赖包
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency
如果你使用lombok注解还报错,说明你的idea还没有安装lombok插件,参考一下这个百度百科教程:
安装完毕后重启idea,lombok注解使用就不会再报错了~
3. 集成mybatis plus
步骤1:
首先我们来看下mybatis plus的官网并且找到整合包:https://mp.baomidou.com/guide/install.html
官网中提示springboot的集成包:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
把mybatis plus导入pom中。然后在application.yml中写入我们的数据源链接信息:
# DataSource Config
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/third-homework?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC
username: root
password: admin
mybatis-plus:
mapper-locations: classpath*:/mapper/**Mapper.xml
好了,我们的mybatis plus已经集成成功。
步骤2:
我们借用官网给我们提供的代码生成工具:https://mp.baomidou.com/guide/generator.html
注意MyBatis-Plus 从 3.0.3 之后移除了代码生成器与模板引擎的默认依赖,需要手动添加相关依赖:
<!--代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.1.0</version>
</dependency>
前面我们已经自动引入了freemaker的集成包,所以这里不需要再重复引入freemaker的引擎依赖。 接着就是修改我们的代码生成器的相关配置,具体修改如下:
// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
package com.example;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
public class CodeGenerator {
/**
* <p>
* 读取控制台内容
* </p>
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotEmpty(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
// gc.setOutputDir("D:\\test");
gc.setAuthor("公众号:java思维导图");
gc.setOpen(false);
// gc.setSwagger2(true); 实体属性 Swagger2 注解
gc.setServiceName("%sService");
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/third-homework?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("admin");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName(null);
pc.setParent("com.example");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";
// 如果模板引擎是 velocity
// String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/src/main/resources/mapper/"
+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
// 配置自定义输出模板
//指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
// templateConfig.setEntity("templates/entity2.java");
// templateConfig.setService();
// templateConfig.setController();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setSuperEntityClass("com.example.entity.BaseEntity");
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
strategy.setSuperControllerClass("com.example.controller.BaseController");
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setSuperEntityColumns("id", "created", "modified", "status");
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix(pc.getModuleName() + "_");
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
注意安装配置来生成BaseEntity、BaseController、还有包路径相关等东西。
BaseEntity:
@Data
public class BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private Date created;
private Date modified;
}
BaseController:
public class BaseController {
@Autowired
HttpServletRequest req;
}
代码生成之后,看到resource/mapper这里,我们把这个null去掉。
修改之后结果:
到这里还没完成,注意一下:还需要配置**@MapperScan****("com.example.mapper")注解。说明mapper的扫描包。**
接下来,我们去写一个小小测试,测试一下mybatis plus有没集成成功,能否查到数据了:
@RunWith(SpringRunner.class)
@SpringBootTest
public class ThirdHomeworkApplicationTests {
@Autowired
UserService userService;
@Test
public void contextLoads() {
User user = userService.getById(1L);
System.out.println(user.toString());
}
}
test之后的结果能够查出数据。注意在mysql中添加一条id为1的数据哈。
4. 集成freemaker
前面我们已经自动集成了freemaker的依赖包,现在我们只需要一些简单配置,其实都不需要配置了:可以看到,我们再写配置的时候springboot已经帮我们完成了很多默认配置,修改自己需要修改的就行了。我这里其实不需要修改,后面需要我们再调整。
spring:
freemarker:
cache: false
然后我们现在去定义一下freemaker的模板。不熟悉freemaker标签的同学可以先去熟悉一下:
首先我们定义个全局layout(宏),用于同一所有的页面。
- templates/inc/layout.ftl
<#-- Layout -->
<#macro layout title>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--[if IE]>
<meta http-equiv='X-UA-Compatible' content='IE=edge,chrome=1'/>
<![endif]-->
<title>${title?default('java思维导图')}</title>
</head>
<body>
<#nested>
</body>
</html>
</#macro>
上面我们用了几个标签,下面我简单讲解一下:
- <#macro layout title> 定义一个宏(模板),名字是loyout,title是参数
- <#nested> 表示在引入loyout这个宏的地方,标签内容的所有内容都会替换到这个标签中。
比如:
- 首页templates/index.ftl
<#include "/inc/layout.ftl"/>
<@layout "首页">
hello world !!
</@layout>
得到的内容其实是:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--[if IE]>
<meta http-equiv='X-UA-Compatible' content='IE=edge,chrome=1'/>
<![endif]-->
<title>首页</title>
</head>
<body>
hello world !!
</body>
</html>
上面我们定义了一个layout宏,这要的好处是一些css、js文件我们直接放到loyout中,然后具体的页面我们直接include,然后在标签<@layout>内写内容即可。 ok,让我们写一个IndexController来跳到这个首页。
@Controller
public class IndexController extends BaseController {
@RequestMapping({"", "/", "/index"})
public String index () {
return "index";
}
}
渲染之后的效果如下:
好了,freemaker可以正常显示页面,现在我们去把我们的博客主题集成进来,我们使用的是layui官方提供的社区模板
先下载下来,解压之后得到:
用idea打开index.html,看下div的层次关系,可以得出以下:
ok,知道了div的层次之后,我们来一一分开来填入内容,该弄成模板的地方就弄成模板,比如头,尾,侧边栏等。我们先把原来的index.html页面的层次分开,分为header.ftl、header-panel.ftl、footer.ftl、right.ftl。然后在layout.ftl中引入我们的js、css文件。得到的结果如下:
- templates/inc/layout.ftl
页面分好之后,得到的打开http://localhost:8080,效果如下:
**注意:**页面中我用到了一个${base}的参数,我是在项目启动时候就初始化的一个项目路径参数,在com.example.config.ContextStartup。大家注意一下。
servletContext.setAttribute("base", servletContext.getContextPath());
5. 优雅的异常处理
有时候不可避免服务器报错的情况,如果不配置异常处理机制,就会默认返回tomcat或者nginx的5XX页面,对普通用户来说,不太友好,用户也不懂什么情况。这时候需要我们程序员设计返回一个友好简单的页面给用户。
处理办法如下:通过使用@ControllerAdvice来进行统一异常处理,@ExceptionHandler(value = Exception.class)来指定捕获的Exception各个类型异常 ,这个异常的处理,是全局的,所有类似的异常,都会跑到这个地方处理。
- com.example.common.exception.GlobalExceptionHandler
- com.example.common.exception.HwException
步骤一、首先我们自定义一个异常HwException,需要继承RuntimeException,这样涉及到事务时候才会有回滚。HwException将作为我们系统catch到错误时候报出来的异常。
public class HwException extends RuntimeException {
private int code;
public HwException() {}
public HwException(int code) {
this.code = code;
}
public HwException(String message) {
super(message);
}
public HwException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
步骤二、定义全局异常处理,@ControllerAdvice表示定义全局控制器异常处理,@ExceptionHandler表示针对性异常处理,可对每种异常针对性处理。
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class)
public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) {
log.error("------------------>捕捉到全局异常", e);
if(e instanceof HwException) {
//...
}
ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
mav.addObject("message", e.getMessage());
mav.addObject("url", req.getRequestURL());
mav.setViewName("error");
return mav;
}
@ExceptionHandler(value = HwException.class)
@ResponseBody
public Result jsonErrorHandler(HttpServletRequest req, HwException e) {
return Result.fail(e.getMessage(), "some error data");
}
}
步骤三、定义异常error页面。打开layui页面,有个tips.ftl的页面比较符合我们的异常页面。可用于展示异常。
- templates/error.ftl
<#include "/inc/layout.ftl"/>
<@layout "首页">
<#include "/inc/header-panel.ftl" />
<div class="layui-container fly-marginTop">
<div class="fly-panel">
<div class="fly-none">
<h2><i class="iconfont icon-tishilian"></i></h2>
<p>${message}</p>
</div>
</div>
</div>
</@layout>
6. 统一的结果返回封装(异步返回)
上面我们用到了一个Result的类,这个用于我们的异步统一返回的结果封装。一般来说,结果里面有几个要素必要的
- 是否成功,可用code表示(如0表示成功,-1表示异常)
- 结果消息
- 结果数据
所以可得到封装如下:
- com.example.common.lang.Result
@Data
public class Result implements Serializable {
private String code;
private String msg;
private Object data;
public static Result succ(Object data) {
Result m = new Result();
m.setCode("0");
m.setData(data);
m.setMsg("操作成功");
return m;
}
public static Result succ(String mess, Object data) {
Result m = new Result();
m.setCode("0");
m.setData(data);
m.setMsg(mess);
return m;
}
public static Result fail(String mess) {
Result m = new Result();
m.setCode("-1");
m.setData(null);
m.setMsg(mess);
return m;
}
public static Result fail(String mess, Object data) {
Result m = new Result();
m.setCode("-1");
m.setData(data);
m.setMsg(mess);
return m;
}
}
首页侧边栏-本周热议
主要是首页或详情页的周热议显示。
本周热议功能
本周热议,本周发表并且评论最多的文章排行,如果直接查询数据库的话很快就可以实现,只需要限定一下文章创建时间,然后更加评论数量倒叙取前几篇即可搞定。
但这里我们使用redis来完成。之前上课时候我们说过,排行榜功能,我们可以使用redis的有序集合zset来完成。现在我们就这个数据结构来完成本周热议的功能。
在编码之前,我们需要先来回顾一下zset的几个基本命令。
- **zrange key start stop [WITHSCORES] **
withscores代表的是否显示顺序号 start和stop代表所在的位置的索引。可以这样理解:将集合元素依照顺序值升序排序再输出,start和stop限制遍历的限制范围
- zincrby key increment member
为有序集 key 的成员 member 的 score 值加上增量 increment 。
- ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]
计算给定的一个或多个有序集的并集,其中给定 key 的数量必须以 numkeys 参数指定,并将该并集(结果集)储存到 destination 。
默认情况下,结果集中某个成员的 score 值是所有给定集下该成员 score 值之 和 。
其他命令可以参考这里:http://doc.redisfans.com/
以下是我做的实验:
我们来分析一下我们的需求。我们想用缓存来完成这本周热议排行榜功能,不依赖数据库(除了初始化数据)。有人发表评论之后,直接使用命令加一,并重新计算并集得到排行榜。
项目启动时候我们先初始化最近文章的评论数量。基本逻辑如下:
- 查库获取最近7天的所有文章(或者加多一个条件:评论数量大于0)
- 然后把文章的评论数量作为有序集合的分数,文章id作为ID存储到zset中。
- 本周热议上有标题和评论数量,因此,我们还需要把文章的基本信息存储到redis总。这样得到文章的id之后,我们再从缓存中得到标题等信息,这里我们可以使用hash的结构来存储文章的信息。
- 另外,因为是本周热议,如果文章发表超过7天了之后就没啥用了,所以我们可以给文章的有序集合一个有效时间。超过7天之后就自定删除缓存。
具体代码如下:
- com.example.service.impl.PostServiceImpl#initIndexWeekRank
/**
* 初始化首页的周评论排行榜
*/
@Override
public void initIndexWeekRank() {
//缓存最近7天的文章评论数量
List<Post> last7DayPosts = this.list(new QueryWrapper<Post>()
.ge("created", DateUtil.offsetDay(new Date(), -7).toJdkDate())
.select("id, title, user_id, comment_count, view_count, created"));
for (Post post : last7DayPosts) {
String key = "day_rank:" + DateUtil.format(post.getCreated(), DatePattern.PURE_DATE_PATTERN);
//设置有效期
long between = DateUtil.between(new Date(), post.getCreated(), DateUnit.DAY);
long expireTime = (7 - between) * 24 * 60 * 60;
//缓存文章到set中,评论数量作为排行标准
redisUtil.zSet(key, post.getId(), post.getCommentCount());
//设置有效期
redisUtil.expire(key, expireTime);
//缓存文章基本信息(hash结构)
this.hashCachePostIdAndTitle(post);
}
//7天阅读相加。
this.zUnionAndStoreLast7DaysForLastWeekRank();
}
- 对应的缓存文章信息的方法如下:
/**
* hash结构缓存文章标题和id
* @param post
*/
private void hashCachePostIdAndTitle(Post post) {
boolean isExist = redisUtil.hasKey("rank_post_" + post.getId());
if(!isExist) {
long between = DateUtil.between(new Date(), post.getCreated(), DateUnit.DAY);
long expireTime = (7 - between) * 24 * 60 * 60;
//缓存文章基本信息(hash结构)
redisUtil.hset("rank_post_" + post.getId(), "post:id", post.getId(), expireTime);
redisUtil.hset("rank_post_" + post.getId(), "post:title", post.getTitle(), expireTime);
//redisUtil.hset("rank_post_" + post.getId(), "post:comment_count", post.getCommentCount(), expireTime);
}
}
- 统计7天的文章集合交集数量:
/**
* 把最近7天的文章评论数量统计一下
* 用于首页的7天评论排行榜
*/
public void zUnionAndStoreLast7DaysForLastWeekRank() {
String prifix = "day_rank:";
List<String> keys = new ArrayList<>();
String key = prifix + DateUtil.format(new Date(), DatePattern.PURE_DATE_PATTERN);
for(int i = -7 ; i < 0; i++) {
Date date = DateUtil.offsetDay(new Date(), i).toJdkDate();
keys.add(prifix + DateUtil.format(date, DatePattern.PURE_DATE_PATTERN));
}
redisUtil.zUnionAndStore(key, keys, "last_week_rank");
}
写好了之后,我们再我们的项目启动类中调用一下即可完成了初始化。
@Slf4j
@Order(1000)
@Component
public class ContextStartup implements ApplicationRunner, ServletContextAware {
private ServletContext servletContext;
@Autowired
PostService postService;
@Override
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
@Override
public void run(ApplicationArguments args) throws Exception {
servletContext.setAttribute("base", servletContext.getContextPath());
//初始化首页的周评论排行榜
postService.initIndexWeekRank();
}
}
以上就完成了初始化。这时候我们在本周热议模块已经可以看到效果了。缓存中已经有我们想要的数据,接下我们在controller中获取出来,然后返回给个我们的页面,页面用异步加载的模式,所以这里定义一个异步接口:
- com.example.controller.PostController
@ResponseBody
@GetMapping("/post/hots")
public Result hotPost() {
Set<ZSetOperations.TypedTuple> lastWeekRank = redisUtil.getZSetRank("last_week_rank", 0, 6);
List<Map<String, Object>> hotPosts = new ArrayList<>();
for (ZSetOperations.TypedTuple typedTuple : lastWeekRank) {
Map<String, Object> map = new HashMap<>();
map.put("comment_count", typedTuple.getScore());
map.put("id", redisUtil.hget("rank_post_" + typedTuple.getValue(), "post:id"));
map.put("title", redisUtil.hget("rank_post_" + typedTuple.getValue(), "post:title"));
hotPosts.add(map);
}
return Result.succ(hotPosts);
}
测试结果:
致此,我们已经完成了获取本周热议的数据,但是,只是一个初始化而已,当有评论的时候还应该添加数据到我们的缓存中,还有页面的内容我们也应该写一些ajax加载数据,这些我们先留着,先到这里,以上是我们之前课程讲过的内容,大家先行消化一下。
注意:本文归作者所有,未经作者允许,不得转载