主要使用的技术:Spring IOC 、DI、SpringBoot、Mybatis、AOP、Spring MVC
整个项目分为 5 个板块:
模块层
:构建出 controller 控制层进行业务逻辑处理后返回的 model 模型,有 Article 文章模型、Category 分类模型、Comment 评论模型、User 用户模型
数据访问层
:构建 @Mapper 注释的接口以及对应的 mapper.xml 配置文件,两者共同形成 Sql Mapper 映射器,建立起对象和数据库之间的映射关系
业务层
:根据构建的 Model 模型分别建立对应的 @Service 业务层 ,通过业务层来和数据访问层交互
控制层
:整体请求处理以及相关业务调用,将处理后的结果返回给相应的视图解析器
视图解析层
:获取经过业务逻辑处理后返回的 Model 模型,将模型中的数据解析并且进行数据渲染,最终呈现给客户端
程序主要功能
:提供用户在自己的账号上进行博客文章的书写,博客内容的修改更新或者删除,也可以在其他人文章下面进行评论
具体业务流程:
-
首先浏览器发送 http 请求,该请求会被 DispatcherServlet 接收到
-
紧接着 DispatcherServlet 会寻找 HandlerMapping 控制器,通过 HandlerMapping 控制器找到对应处理的 Controller,这其中需要配合 RequestMapping 注解来达到地址映射的功能
RequestMapping 可以注解在类上或者方法上,如果注解在类上,那么该 RequestMapping 的 value 就是该类全部地址映射的父路径,类中方法上注解的 RequestMapping 路径为子路径,映射地址的时候,只有父路径 + 子路径和请求地址相同的时候才能完成地址映射
- 在对应的 Controller 控制器中,通过 @Autowired 自动注入 Service 业务层的 Bean 实例并且通过业务层和数据库进行交互得到数据结果,或者获取 http 请求中的有用数据(例如用户id,文章id等),将这些数据设置到 Model 模型当中,最终将模型返回给对应的视图解析器
Mybatis
在业务层和数据访问层交互时,通过 Mybatis 技术完成 java对象自动持久化到数据库中,其中涉及到 @Mapper 接口+mapper.xml 文件组成的 Sql Mapper 映射器,SqlSessionFactory,SelSession,MapperStatement 核心组件
@Mapper 注释的接口要先利用 AOP 切面技术的思想和 Mybatis 框架来具体形成代理类,建立起 Mapper 接口和 mappper.xml 配置文件的映射关系
映射关系:
(1)一个 Mapper 接口映射到一个 mapper.xml 文件
(2)一个 Mapper 接口中的方法映射 mapper.xml 文件中的一个增删查改节点,也就是对应一个封装好的MapperStatement
(3)mapper.xml文件中的 resultMap 表示结果映射集, 元素的 type 属性表示需要的 POJO,id 属性是 resultMap 的唯一标识
<resultMap id="BaseResultMap" type="frank.model.Article" >
<id column="id" property="id" jdbcType="BIGINT" /> // id 表示用哪个列作为主键
<result column="user_id" property="userId" jdbcType="BIGINT" />
<result column="status" property="status" jdbcType="TINYINT" />
<result column="title" property="title" jdbcType="VARCHAR" />
<result column="created_at" property="createdAt" jdbcType="TIMESTAMP" />
<result column="comment_count" property="commentCount" jdbcType="INTEGER" />
<association property="author" resultMap="frank.mapper.UserMapper.BaseResultMap">
<id column="user_id" property="id" jdbcType="BIGINT"/>
</association>
</resultMap>
type 表示的 java普通对象中的属性需要和里面的 property 标签一一对应起来
id 标签表示用哪个列作为主键,column 表示对应的 java对象属性在 mapper.xml 文件中字面量
建立起映射关系之后,通过调用接口中的方法,选择对应映射的 MapperStatement ,接着将传入的参数替换掉SQL语句中的字面量,执行SQL语句操作数据库,返回的结果也需要映射成对应的 java对象
- 视图解析器(也就是对应的 freemarker 文件:Freemarker是一种动态网页技术框架,其他还有如JSP、velocity等)将传入的 Model 解析数据,并且将数据进行动态渲染,最终展示给客户端
详细的过程:
一
通过 SpringBoot 技术,将一些框架或者技术所需的配置进行了整合,这样就减少了大量前期的 xml 配置信息的工作,启动 SpringBootApplication 所注解的程序,这样整个程序就可以快速的启动
二
开始装配 Bean ,本项目中使用自动化配置的方式装配 Bean(还可以通过 java 显式装配和 xml 文件装配 Bean)
(1) 在 SpringBootApplication 的注解中就包含了 ComponentScan 的功能,扫描 Application 所在的路径下所有的类,注释有@Component、@Service、@Controller、@Resource、@Autowired 等类就会创建其 Bean 实例到 Spring 容器中,在 Application 启动的时候就创建这些 Bean 实例(默认是 Bean 实例的 scope 为 singleton ,其中 lazy-init 也为 false,没有启动延迟加载策略)
(2)Bean 实例化之后,Spring 自动满足 Bean 实例之间的依赖,通过 @Autowired 注解将依赖注入到 Bean 实例中(@Autowired 内部原理是通过反射来实现),例如将 Service 层的ArticleService、CommentService等注入到 Controller 层中
三
通过自动化配置装配 Bean 后,就要开始 http 请求的处理,使用 Mybatis 技术方便快捷的完成数据库操作
在除了首页、用户登录页面,用户注册页面,一些静态资源的访问(例如图片等)还有未登录但是可以查看谋篇文章的评论信息,其他情况下的 http 请求都需要拦截,只有用户注册登录了才能进行文章的评论、发布以及更新等权限操作,所以要设置一个拦截器
拦截器
:
该内容的编写不是通过自动化配置的方式装配Bean,因为此时需要用到第三方库(例如:HandlerInterceptor)的功能类,而我们不能在这些类上加入 @Component 或者 @Controller 这类注释来注册 Bean 实例,所以需要通过 Java 显式配置的方式加载 Baen
方法
:使用 JavaConfig 来帮助完成,这部分代码通常是启动配置类的作用,不写一些业务逻辑的代码,所以常常需要写在一个独立的包中区别开
@Configuration
public class BlogConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// ** 两个 * 号匹配多级路径,而一个 * 号匹配一级路径
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns("/css/**")
.excludePathPatterns("/fonts/**")
.excludePathPatterns("/js/**")
.excludePathPatterns("/plugins/editor/**")
.excludePathPatterns("/images/**")
.excludePathPatterns("/")
.excludePathPatterns("/login")
.excludePathPatterns("/register")
.excludePathPatterns("/a/*");
}
}
@Configuration 注解的类好比如 xml 配置文件,但是其内部可以通过 @Bean 这样的注解来完成 Bean 实例化,比起 xml 文件更加方便
BlogConfig:继承了WebMvcConfigurer 实现对于 http 请求的拦截,除了 excludePathPatterns 内容的 url ,其他部分都会被拦截,在 LoginInterceptor 内定义的拦截后处理的逻辑:通过 session 获取用户信息,如果用户已经登录成功,那么允许该请求通过,否则重定向到登录页面
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession(false);
if (session != null) {
Object user = session.getAttribute("user");
if (user != null) {
return true;
}
}
response.sendRedirect("/login");
return false;
}
}
(1)未登录页面
URL:Localhost:8080/
ArticleController 层代码为:
@Autowired
private CommentService commentService;
@RequestMapping("/")
public String index(Model model) {
List<Article> articles = articleService.queryArticles();
model.addAttribute("articleList", articles);
return "index";
}
可以看到页面的基本情况:
此时还没有进行用户登录,但是可以看点击某个文章的查看全文,就可以看到该篇文章低下的评论信息
(2)register 页面
URL :localhost:8080/register
UserController 层代码为:
@Autowired
private UserService userService;
@RequestMapping("/register")
public String register(String username, String password, String nickname) {
// 如果在注册是用户名、名称或者密码有填写为空的情况,则表示注册失败需要重新注册
if (username == null || password == null || nickname == null) {
return "register";
}
User user = new User();
user.setNickname(nickname);
user.setUsername(username);
user.setPassword(password);
user.setAvatar("https://picsum.photos/id/1/200/200");
userService.insert(user);
return "login";
}
注册的过程:
首先点击注册按钮,浏览器显示 register.ftlh Freemarker文件内容,用户输入完注册信息后,点击注册提交按钮,这时就会发送一个 htttp 请求到服务端,请求的 url 为:localhost:8080/register 并且里面包含了输入的 data 数据,映射到 UserController 层的 register 方法进行处理,User 就是 Controller 层处理后返回的 Model,将 User 属性封装好了传入对应的 Service 层,让业务层和数据访问层进行交互,业务层调用 Mapper 接口的 insert 方法,之所以能使用接口的方法是因为此时是由该接口的代理对象操作,代理对象将数据传入封装好的 MapperStatement ,将 Sql 语句的字面量映射成真实数据,然后执行 SQL 语句逻辑操作数据库,最后的返回结果也需要映射成 java对象或者其他数据类型
(3)login页面
URL:http://localhost:8080/login
UserController 层代码为:
@Autowired
private UserService userService;
@RequestMapping("/login")
public String login(String username, String password, HttpServletRequest request) {
// 用户名和密码的简单校验,如果为空则重新登录,登录成功跳转至首页(“/”)
if (username == null || password == null) {
return "login";
}
User user = userService.login(username, password);
if (user == null) {
return "register";
} else {
// 默认构造函数参数为 true, 如果不存在 session 则创建一个
HttpSession session = request.getSession();
session.setAttribute("user", user);
return "/";
}
}
过程和用户注册类似,注意:用户登录成功,在 session 中设置用户的信息,方便其他功能取出用户信息操作,最后返回到程序的首页
(4)Comment页面
URL:http://localhost:8080/a/{id}/comments
CommentController 层代码为:
@Autowired
private CommentService commentService;
@RequestMapping(value = "/a/{id}/comments", method = RequestMethod.POST)
public String addComment(@PathVariable("id") Long id, String content) {
Comment comment = new Comment();
comment.setArticleId(id);
comment.setContent(content);
comment.setCreatedAt(new Date());
int num = commentService.insert(comment);
return "redirect:/a/" + id; // 重定向
}
URL中的最后一个数字代表该篇文章的用户 id ,通过 {} + @PathVariable() 来动态的获取 url 中的参数,在 CommentMapper.xml 中 insert 方法的需要该篇文章作者的 id ,仅通过 url 中的参数是无法获取的,所以要建立用户和评论的一对一连接,这样通过文章 id 就能查找到用户的 id
<association property="user" resultMap="frank.mapper.UserMapper.BaseResultMap">
<id column="user_id" property="id" jdbcType="BIGINT"/>
</association>
(5)Writer页面
URL:http://localhost:8080/writer
ArticleController 层代码为:
@Autowired
private CategoryService categoryService;
@Autowired
private ArticleService articleService;
@RequestMapping("/writer")
public String writer(HttpSession session, Long activeCid, Model model) {
User user = (User)session.getAttribute("user");
List<Article> articles = articleService.queryArticleByUserId(user.getId());
model.addAttribute("articleList", articles);
List<Category> categories = categoryService.queryCategoriesByUserId(user.getId());
model.addAttribute("categoryList" ,categories);
model.addAttribute("activeCid" ,activeCid == null ? categories.get(0).getId() : activeCid);
return "writer";
}
首先通过 session 获取到之前设置的 user 用户信息,然后让业务层 ArticleService 和数据访问层交互,通过 Mybatis 生成的 ArticleMapper 代理对象具体操作,由 MapperMethod 也就是接口中方法的封装,它里面通过 SqlCommont 和 MethodSignature 两个内部类具体实现,MethodSignature 主要作用就是封装了 Mapper 接口中方法的参数类型、返回值类型 等信息,SqlCommont 主要作用是封装底层的增删改查操作,确切来讲这一部分的内容跟 Mapper.xml 文件中的select、delete节点等有关,MyBatis 会把每一个节点(如:select节点、delete节点)生成一个MappedStatement,然后通过映射输入,将输入的参数映射对应 Sql 语句中的字面量,最终输出结果也需要映射成相应的接口中方法返回值类型
如何能够将 select 到的多条信息映射到 List 中
因为MethodSignature就封装了 Mapper 接口中方法的参数类型、返回值类型 等信息,通过 Method.getReturnType 等方法就能获取到相应的方法返回值,具体在 SqlCommont 中会判断选择应该返回的对应数据类型
Model
:model 是一个接口,它被 ExtendedModelMap 类实现,而 ExtendedModelMap 类又继承了 LinkedHashMap ,以键值对的形式来存储数据,最终将 freeMarker 所需要的对象全部设置进去后,返回 Model 给视图解析器,让解析器对数据进行渲染最终呈现回浏览器
activeCid
:如果用户是先点击了分类栏中的某一类,表示现在进入该类文章中,此时再次点击新建文章,那么后面创建的文章就会属于该类型,activeCid 就是分类的 id 号,如果没有点击分类,那么就将新建的文章加入默认这一栏
新建一个分类
URL:http://localhost:8080/c/add
CategoryController 层代码:
@Autowired
private CategoryService categoryService;
@RequestMapping(value = "/c/add", method = RequestMethod.POST)
public String addCategory(HttpSession session, Category category) {
User user = (User) session.getAttribute("user");
category.setUserId(user.getId());
int num = categoryService.insert(category);
return "redirect:/writer";
}
(6)Editor页面
URL:http://localhost:8080/writer/forward/{type}/{id}/editor
ArticleController 层代码为:
@Autowired
private CategoryService categoryService;
@Autowired
private ArticleService articleService;
@RequestMapping("/writer/forward/{type}/{id}/editor")
public String editorAdd(@PathVariable("type") Integer type,
@PathVariable("id") Long id, Model model) {
Category category;
if (type == 1) {
// 完成 editor 页面新增的属性设置
category = categoryService.queryCategoryById(id);
model.addAttribute("activeCid", id);
} else if (type == 2) {
// 完成 editor 页面修改的属性设置
Article article = articleService.queryArticle(id);
model.addAttribute("article", article);
category = categoryService.queryCategoryById(new Long(article.getCategoryId()));
} else {
// 删除功能:直接重定向
return String.format("redirect:/writer/article/3/{id}", id) ;
}
model.addAttribute("type",type);
model.addAttribute("category", category);
return "editor";
}
首先需要通过@PathVariable 和 {} 动态的获取到 url 中的参数信息
(1)如果是从 从 Writer 点击了
新键文章
那么就进入一个新建文章页面,此时参数中的 type 为 1 并且 id 是 activeCid 也就是分类 id,这样新增的文章就可以归属于该类栏目,直接通过 activeCid 获取到 Category 信息
(2)如果是从 Writer 页面点击了
修改文章
,那么就会进入文章修改的页面,此时 type 为 2 并且 id 是 文章 id,通过文章 id 先获取到该篇修改文章的内容,设置入 Model ,并且从文章中获取到 activeCid ,接着借助 activeCid 获取 Category 信息
无论是新建文章还是修改文章,都需要知道 Category 信息,最终都是要设置到 Model 中,最终将 Model 返回给对应的视图解析器处理,最终呈现给浏览器
(3)
删除文章
:直接重定向到功能页面,
(7)功能(新增、删除、修改)页面
URL:http://localhost:8080/writer/article/{type}/{id}
ArticleController 层代码为:
@Autowired
private ArticleService articleService;
@RequestMapping(value = "/writer/article/{type}/{id}", method = {RequestMethod.POST, RequestMethod.GET})
public String publis(@PathVariable("type") Integer type,
@PathVariable("id") Integer id, Article article,
HttpSession session) {
article.setUpdatedAt(new Date());
if (type == 1) {
// 新增的时候,插入文章数据
article.setCategoryId(id);
User user = (User)session.getAttribute("user");
article.setUserId(user.getId());
article.setCoverImage("https://picsum.photos/id/1/400/300");
article.setCreatedAt(new Date());
article.setStatus((byte)0);
article.setViewCount(0L);
article.setCommentCount(0);
int num = articleService.insert(article);
// 获取新增文章id,因为如果时新增操作,那么传入的id是分类的id,所以在完成新增操作后,要获取文章id
id = article.getId().intValue();
} else if (type == 2) {
// 修改的时候,修改文章的数据
article.setId(new Long(id));
int num = articleService.updateByCondition(article);
} else {
// 删除功能
articleService.deleteByPrimaryKey(new Long(id));
}
return "redirect:/writer";
}
首先需要通过@PathVariable 和 {} 动态的获取到 url 中的参数信息
(1)
新增文章
:type 为 1 并且 id 表示 activeCid 分类 id ,将 Article 模型所需属性全部设置进去,通过业务层 ArticleService 与数据访问层交互,将新文章添加到数据库中
(2)
修改文章
:type 为 2 并且 id 表示 文章 id ,此时修改的 title 或者 content 都以及自动的装配到 article 中(默认表单提交的方式,提交的字段名,springmvc会自动装配进传入对象的属性中),通过 Mybatis 中的 if 标签来完成 Sql 语句操作,将文章修改过的内容更新,其余的不变
(3)
删除文章
:type 为 3 ,通过业务层和数据范文层交互,直接调用 Mapper 接口中的删除方法,通过 MapperStatement 替换字面量并且执行 SQL 语句操作数据库,删除该文章内容,因为在 Writer 页面点击删除按钮后回直接重定向到功能页面,默认发送的 http 请求是 GET 方法,所以要在在 @RequestMapping 中加入 GET 方法:
个人总结:
(1)整体的项目逻辑不难理解,通过整理各个层次的主要功能以及层次之间的相互交互情况,就能理清整个博客项目的来龙去脉
(2)主要技术:通过 SpringBoot 框架集合了其他许多的框架的配置,能够简洁快速的建立起一个 Spring框架的项目,省去了冗杂的xml配置资源文件等代码,通过 SpringMVC 框架搭建 Web 服务器环境,让整体项目中的各个层次、角色划分清晰,紧接着通过自动化转配 Bean ,完成每个 Bean 实例需要的依赖注入工作,最后在业务逻辑处理过程中,最后利用 Mybatis 半自动映射框架构建业务层和数据访问层的交互任务
(3)项目过程坎坷:虽然整体实现功能就是平常的写博客,修改博客,评论,删除博客功能,但是具体实现更考验对其流程的理解,每个点击都有它的“归属”,每个发布都有它的“展示”,往往会在以为就要达到最终效果时,才发现还有许多功能尚未添加