后端代码编写
前言
- 这实现这篇博客之前,我学习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和并发,设计模式,网络编程,冲冲冲