本文系Vue & SpringBoot从零实现博客系统第五部分 后端代码编写
后端代码编写
前言
- 这实现这篇博客之前,我学习ssm框架不到两个星期,然后看了一天springboot就开始了,中途遇到很多坑,所以就记下来和大家分享
- 本来想着继承redis和SpringSecurity,但是由于只有我一个管理员,所以没必要集成SpringSecurity。对于redis来说,因为想赶快出一个能上线的博客,所以一切从简,就没有集成redis
工具 & 插件 & 依赖 & 技术栈
- MySQL 8.0 + (这个8.0和5.0好像没什么区别)
- Maven (强大的项目管理工具,依赖包的获取,项目的打包等等一件完成)
- Intellij IDEA (Jetbrain家族一员,出色的Java IDE)
- Mybatis(持久层ORM框架,结合官方文档非常容易上手)
- PageHelper(一个Mybatis插件,用于分页)
- SpringBoot(简化了Spring和SpringMVC的配置,适合快速开发项目工程)
项目
项目结构
- aop 程序切面,用于日志业务
- controller 控制层,接受前端的request,并返回response
- dao 持久成,通过SQL和数据库交互
- domain 实体类,通过mybatis逆向生成
- generator mybatis逆向的启动类,这个在项目部署上可以删去
- interceptor 拦截器,用于权限处理
- service 服务层,处理业务逻辑
- impl 实现类
- 接口
- util 一些工具类,如获取ip对应地址,时间格式转换等等
- resources / mapoer mybatis需要的xml文件
- application.properties springboot的配置
- generatorConfig.xml mybatis的逆向配置类
项目pom
<?xml version="1.0" encoding="UTF-8"?>
<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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.zi10ng</groupId>
<artifactId>blog</artifactId>
<version>0.0.4-SNAPSHOT</version>
<name>blog</name>
<packaging>jar</packaging>
<description>blog for zi10ng</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<!--MyBatis逆向工程-->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.6</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
<scope>runtime</scope>
</dependency>
<!--junit-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--SpringBoot热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional> <!-- 这个需要为 true 热部署才有效 -->
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<!--aop-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
<!--myPages-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.12</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>cn.zi10ng.blog.BlogApplication</mainClass>
</configuration>
</plugin>
<!--mybatis逆向的插件-->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.2</version>
<configuration>
<configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-generator</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
业务流程
当tomcat接收到一个request时,先经过拦截器,拦截器过滤之后到controller层接收请求,之后再到service处理业务逻辑,再之后到dao层与数据库交互,获取数据,再返回。业务逻辑非常简单
项目难点
接口数据结构的封装
-
在谷歌的过程中,发现好多人都用阿里的fastjson,我嫌麻烦就没有,springboot自带的有jackson我觉着也挺好用的
-
controller接收request时,可能接收一个实体类,也可能接收一个id,还可能接收多个参数(通过map)
-
controller返回response时,可返回一个参数,也可能返回一个实体类,还可能返回多个实体类(通过map)
-
可以参考下这个博客
-
如下示例
@GetMapping("/any/article") public List<Object> getArticle(long id) { List<Object> list = new ArrayList<>(); TreeUtils treeUtils = new TreeUtils(); list.add(articleService.getArticleContentById(id)); list.add(treeUtils.buildTree(articleService.listCommentOfArticle(id), id)); list.add(categoryInfoService.listCategoryNameByArticleId(id)); articleService.updateArticleInfo(id, "traffic", true); return list; }
@PostMapping("/admin/postArticle") public boolean postArticle(@RequestBody Map<String, Object> map) throws IOException { ObjectMapper objectMapper = new ObjectMapper(); ArticleInfo articleInfo = objectMapper.readValue( objectMapper.writeValueAsString(map.get("articleInfo")), ArticleInfo.class); ArticleContent articleContent = objectMapper.readValue( objectMapper.writeValueAsString(map.get("articleContent")), ArticleContent.class); ArticleCategory articleCategory = objectMapper.readValue( objectMapper.writeValueAsString(map.get("articleCategory")), ArticleCategory.class); if (articleService.insertArticle(articleInfo, articleContent, articleCategory)){ categoryInfoService.updateCategoryNumById(articleCategory.getCategoryId(), 1); return true; } return false; }
pagehelper的应用
pagehelper作为mybatis的一个插件,对程序的耦合度,就我来说,还是非常高的。比如,在service层中程序程序必须要这样写:
@Override
public List<ArticleInfo> listArticleInfoByTime(MyPages myPages) {
/**代码段**/
// 下面是必须要求这样写的,这就意味着我不能在Service处理相关的业务逻辑
PageHelper.startPage(myPages.getPage(), myPages.getSize());
return articleInfoMapper.listArticleInfoByTime();
}
在写程序的过程中,有时候就会遇到上面的问题,这就要求
-
要么有一个好的SQL,
-
要么在controller处理逻辑。当在controller处理逻辑的时候,我又对其封装了一层(妈呀中间件真的是太强了),即当其返回pagehelper后,又需要逻辑处理的情况下,可以再用一个pagehelper封装刚才的pagehelper,并处理逻辑。如下
@GetMapping("/any/byTime") public PageInfo<ArticleInfoCategory> listArticleInfoByTime(MyPages myPages) { // 也可用mybatis 一对多查询 PageInfo<ArticleInfo> pageInfo = new PageInfo<>(articleService.listArticleInfoByTime(myPages)); return PageInfo2PageInfo.article2ArticleCategory(pageInfo, categoryInfoService); }
多级评论/分类的实现
有两种实现方式
- 一种是直接通过SQL,之前写过sqllite的,但是Mysql的递归我不会写QAQ
- 第二种就是从数据库拿出数据之后变成一个list,然后再把list按照规律构建成一个树,再作为response返回给前端。我采取第二种
实现方式
在这里我用了一个构建为树的工具类
package cn.zi10ng.blog.util;
import cn.zi10ng.blog.domain.CommentInfo;
import cn.zi10ng.blog.domain.Node;
import java.util.ArrayList;
import java.util.List;
/**
* @author Zi10ng
* @date 2019年8月24日21:20:16
*/
public class TreeUtils {
private List<Long> longs = new ArrayList<>();
private Node nodeMe = new Node();
/**
* 把评论信息的集合转化为一个树
*/
public Node buildTree(List<CommentInfo> commentInfo, long id){
Node tree = new Node();
List<Node> children = new ArrayList<>();
List<Node> nodeList = new ArrayList<>();
for (CommentInfo info : commentInfo) {
children.add(buildNode(info));
}
tree.setId(id);
tree.setChildren(children);
for (Node child : children) {
Node node = findNode(children, child.getParentId());
List<Node> nodes = new ArrayList<>();
if (node != null) {
if (node.getChildren() != null) {
nodes = node.getChildren();
nodes.add(child);
node.setChildren(nodes);
}else {
nodes.add(child);
node.setChildren(nodes);
}
nodeList.add(child);
}
}
for (Node node : nodeList) {
children.remove(node);
}
return tree;
}
/** 把树转换为list
* @param node 节点
* @return list
*/
public List<Long> travelSubTree(Node node){
//如果不是父节点的话
if(node.getChildren() != null) {
for (Node index : node.getChildren()) {
longs.add(index.getId());
if (index.getChildren() != null && index.getChildren().size() > 0 ) {
travelSubTree(index);
}
}
}
return longs;
}
/**
* 拿到某一节点的树以及其子节点
* @param node 树节点
* @param id 标识id
* @return node
*/
public Node travelTree(Node node, long id){
if(node != null) {
for (Node index : node.getChildren()) {
if (index.getId() == id){
return index;
}
if (index.getChildren() != null && index.getChildren().size() > 0 ) {
nodeMe = travelTree(index, id);
}
}
}
return nodeMe;
}
private Node findNode(List<Node> nodes, long id){
for (Node node : nodes) {
if (node.getId() == id) {
return node;
}
}
return null;
}
private Node buildNode(CommentInfo info){
Node node = new Node();
node.setId(info.getId());
node.setParentId(info.getParentId());
node.setObject(info);
node.setChildren(null);
return node;
}
}
Mybatis逆向工具
对于构建一些小项目来说,mybatis的逆向还是很有用的,它会生成对应的domain和简单的mapper
- 需要引入maven插件和依赖,具体的依赖已经在上面的pom中列出来了,就不详细说了
配置的xml如下
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="DB2Tables" targetRuntime="MyBatis3">
<!--避免生成重复代码的插件-->
<plugin type="cn.zi10ng.blog.util.OverIsCombinedPlugin"/>
<!--是否在代码中显示注释-->
<commentGenerator>
<property name="suppressDate" value="true"/>
<property name="suppressAllComments" value="true"/>
</commentGenerator>
<!--数据库链接地址账号密码-->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/world?useSSL=false&serverTimezone=Hongkong&characterEncoding=utf-8&autoReconnect=true"
userId="root"
password="root">
</jdbcConnection>
<!--生成pojo类存放位置-->
<javaModelGenerator targetPackage="cn.zi10ng.blog.domain" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!--生成xml映射文件存放位置-->
<sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>
<!--生成mapper类存放位置-->
<javaClientGenerator type="XMLMAPPER" targetPackage="cn.zi10ng.blog.dao" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>
<!--生成对应表及类名-->
<table tableName="sys_log" domainObjectName="SysLog" enableCountByExample="false"
enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="true"
selectByExampleQueryId="false">
<!--使用自增长键-->
<property name="my.isgen.usekeys" value="true"/>
<!--使用数据库中实际的字段名作为生成的实体类的属性-->
<property name="useActualColumnNames" value="true"/>
<generatedKey column="id" sqlStatement="JDBC"/>
</table>
<table>
....
</table>
...
</context>
</generatorConfiguration>
启动类
package cn.zi10ng.blog.generator;
import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.internal.DefaultShellCallback;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @author Zi10ng
* @date 2019年7月24日18:01:22
* mybatis的逆向
*/
public class MybatisGenerator {
public static void main(String[] args) throws Exception {
String today = "2019-07-24";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date now = sdf.parse(today);
Date d = new Date();
if (d.getTime() > now.getTime() + 1000 * 60 * 60 * 24) {
System.err.println("——————未成成功运行——————");
System.err.println("——————未成成功运行——————");
System.err.println("本程序具有破坏作用,应该只运行一次,如果必须要再运行,需要修改today变量为今天,如:" + sdf.format(new Date()));
return;
}
List<String> warnings = new ArrayList<>();
boolean overwrite = true;
InputStream is = MybatisGenerator.class.getClassLoader().getResource("generatorConfig.xml").openStream();
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(is);
is.close();
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
myBatisGenerator.generate(null);
System.out.println("生成代码成功,只能执行一次,以后执行会覆盖掉mapper,pojo,xml 等文件上做的修改");
}
}
IP解析接口
这里我用了一个免费网站的ip解析接口,将ip解析为地址,具体使用方式为:
package cn.zi10ng.blog.util;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Zi10ng
* @date 2019年9月9日21:24:13
* 把ip解析为地区
*/
public class Ip2Region {
public static String sendGet(String ip){
try {
String url = "http://api.online-service.vip/ip3?ip=";
URL query = new URL(url + ip);
HttpURLConnection conn = (HttpURLConnection) query.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuilder sb= new StringBuilder();
String regEx = "[\\u4e00-\\u9fa5]";
Pattern p = Pattern.compile(regEx);
Matcher m = p.matcher(br.readLine());
while(m.find()){
sb.append(m.group());
}
return String.valueOf(sb);
} catch (java.io.IOException e) {
e.printStackTrace();
}
return null;
}
}
一些SQL语句
我们知道,mybatis的SQL映射可使用注解,也可以使用xml。通常情况下,我们一般用注解写比较简单的,xml写比较复杂的,比如if子句等等。但事无绝对,只要文档看的好,注解反而看着更简洁。
一个好的SQL可以帮我们省去很多代码逻辑
-
返回自增id
<insert id="insertArticleInfo" parameterType="cn.zi10ng.blog.domain.ArticleInfo" useGeneratedKeys="true" keyProperty="id"> insert into article_info (title, summary) values (#{title}, #{summary}) </insert>
之后如果有问题可以参考这篇博客,这也是一个小坑
-
注解方式实现foreach
/** * 按分类,时间降序查询全部文章 * @param categoryId 分类id * @return list */ @Select({"<script>", "select a.*" + "from article_info as a, article_category as ac,category_info as c ", "where c.id = ac.category_id and a.id = ac.article_id ", "and c.id in", " <foreach item= 'categoryId' index= 'index' collection= 'list'", " open='(' separator=',' close=')'>", " #{categoryId}", " </foreach>", "order by create_by desc", "</script>"}) @ResultMap("ArticleInfoMap") List<ArticleInfo> listArticleInfoByCategory(List<Long> categoryId);
-
如果存在该ip则更新,不存在则创建(这个用于用户的注册和登录)
<insert id="postUser" parameterType="cn.zi10ng.blog.domain.SysUser"> INSERT INTO sys_user( role , browser , region, ip) VALUES( #{role} , #{browser} , #{region}, #{ip}) ON DUPLICATE KEY UPDATE <if test="name != null"> name = #{name}, connect = #{connect}, role = #{role} </if> <if test="name == null"> num = num + 1 </if> </insert>
日期格式的处理
-
第一点,我们需要用合适的SQL中的data函数去查询出日期(这一步需要读者自行查看相关SQL函数)
-
第二点,在实体类的get/set方法中,需要对日期格式进行进一步的处理,把日期改为字符串格式
public String getCreateByStr() { if (createBy != null){ createByStr = DateFormatUtils.data2String(createBy,"yyyy-MM-dd HH:mm:ss"); } return createByStr; } public void setCreateByStr(String createByStr) { this.createByStr = createByStr; } public String getModifiedByStr() { if (modifiedBy != null){ modifiedByStr = DateFormatUtils.data2String(modifiedBy,"yyyy-MM-dd HH:mm:ss"); } return modifiedByStr; } public void setModifiedByStr(String modifiedByStr) { this.modifiedByStr = modifiedByStr; } }
日期工具类如下:
package cn.zi10ng.blog.util; import java.text.SimpleDateFormat; import java.util.Date; /** * @author Zi10ng * @date 2019年7月24日20:54:54 * 日期转换工具类 */ public class DateFormatUtils { /** * 日期转换为字符串 * @param date 日期 * @param str 字符串 * @return 字符串 */ public static String data2String(Date date, String str){ SimpleDateFormat sdf = new SimpleDateFormat(str); return sdf.format(date); } }
Servlet的拦截器处理权限
前文已经说到,因为只有我一个管理员,所以没必要用功能强大的SpringSecurity或者小而精的shiro,直接通过过滤器拦截一下就完事了
-
拦截器的功能就是检测request的URL,看是否含有/admin(这在本系列文章第三部分接口设计中已经提到,如果含有/admin,则说明是管理员权限的接口),如果含有/admin,则查看其是否有请求头中是否有token,且token是否和服务器中保存的一样,如果一样,则放行,不一样则把null作为response返回
package cn.zi10ng.blog.interceptor; import cn.zi10ng.blog.service.UserService; import cn.zi10ng.blog.util.Md5Utils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @author stalern * @date 2019年9月17日19:01:09 * 拦截器 */ public class AuthenticationInterceptor implements HandlerInterceptor { @Autowired UserService userService; @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception { String admin = "admin"; // 从 http 请求头中取出 token String tokenRaw = httpServletRequest.getHeader("token"); String uri = httpServletRequest.getRequestURI(); //如果路径中包含admin if (uri.contains(admin)) { if (tokenRaw == null) { return false; } else { // 检查token是否和服务器中的token相同 } } return true; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } }
-
之后还需要配置拦截器,如下
package cn.zi10ng.blog.interceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @author stalern * @date 2019年9月17日19:16:59 * 拦截器配置类 */ @Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticationInterceptor()) .addPathPatterns("/**"); } @Bean public AuthenticationInterceptor authenticationInterceptor() { return new AuthenticationInterceptor(); } }
一些教训
- 前后端分离一定要分装状态码,状态,数据
- 合理使用SQL语句可以减少业务层的代码
- 整个项目的完工,让我发现了curd真的很累,一个强大的程序员,必须要掌握基础和底层,下一步,jvm和并发,设计模式,网络编程,冲冲冲