(eblog)1、项目架构搭建、本周热议

小助手 1年前 ⋅ 8018 阅读

项目基本架构搭建

以下过程,我们以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/

以下是我做的实验:

图片

我们来分析一下我们的需求。我们想用缓存来完成这本周热议排行榜功能,不依赖数据库(除了初始化数据)。有人发表评论之后,直接使用命令加一,并重新计算并集得到排行榜。

项目启动时候我们先初始化最近文章的评论数量。基本逻辑如下:

  1. 查库获取最近7天的所有文章(或者加多一个条件:评论数量大于0)
  2. 然后把文章的评论数量作为有序集合的分数,文章id作为ID存储到zset中。
  3. 本周热议上有标题和评论数量,因此,我们还需要把文章的基本信息存储到redis总。这样得到文章的id之后,我们再从缓存中得到标题等信息,这里我们可以使用hash的结构来存储文章的信息。
  4. 另外,因为是本周热议,如果文章发表超过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加载数据,这些我们先留着,先到这里,以上是我们之前课程讲过的内容,大家先行消化一下。


全部评论: 0

    我有话说: