个人博客

  • Post author:
  • Post category:其他


主要使用的技术:Spring IOC 、DI、SpringBoot、Mybatis、AOP、Spring MVC



整个项目分为 5 个板块:


模块层

:构建出 controller 控制层进行业务逻辑处理后返回的 model 模型,有 Article 文章模型、Category 分类模型、Comment 评论模型、User 用户模型


数据访问层

:构建 @Mapper 注释的接口以及对应的 mapper.xml 配置文件,两者共同形成 Sql Mapper 映射器,建立起对象和数据库之间的映射关系


业务层

:根据构建的 Model 模型分别建立对应的 @Service 业务层 ,通过业务层来和数据访问层交互


控制层

:整体请求处理以及相关业务调用,将处理后的结果返回给相应的视图解析器


视图解析层

:获取经过业务逻辑处理后返回的 Model 模型,将模型中的数据解析并且进行数据渲染,最终呈现给客户端


程序主要功能

:提供用户在自己的账号上进行博客文章的书写,博客内容的修改更新或者删除,也可以在其他人文章下面进行评论



具体业务流程:

  1. 首先浏览器发送 http 请求,该请求会被 DispatcherServlet 接收到

  2. 紧接着 DispatcherServlet 会寻找 HandlerMapping 控制器,通过 HandlerMapping 控制器找到对应处理的 Controller,这其中需要配合 RequestMapping 注解来达到地址映射的功能

RequestMapping 可以注解在类上或者方法上,如果注解在类上,那么该 RequestMapping 的 value 就是该类全部地址映射的父路径,类中方法上注解的 RequestMapping 路径为子路径,映射地址的时候,只有父路径 + 子路径和请求地址相同的时候才能完成地址映射

  1. 在对应的 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对象

  1. 视图解析器(也就是对应的 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)项目过程坎坷:虽然整体实现功能就是平常的写博客,修改博客,评论,删除博客功能,但是具体实现更考验对其流程的理解,每个点击都有它的“归属”,每个发布都有它的“展示”,往往会在以为就要达到最终效果时,才发现还有许多功能尚未添加



版权声明:本文为qq_45239139原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。