Spring实现博客系统

     在上次用Servlet实现了博客系统之后,一直觉得代码写起来比较繁琐,而且耦合度很高。直到学习了Spring,我又看到了一线生机,运用SpringBoot重新改造了我的博客系统,接下来讲讲Spring是个什么东西,并把我的改造思路给大家分享下。 

1、什么是Spring
Spring简直是个神奇的东西,它向所有的对象提供它所需的东西,同时也把对象所需要的东西提供给对方,大大降低了代码之间的耦合度。所有对象的销毁和创建都由Spring掌控,在Spring中有个主要概念,就是控制反转,那么什么是控制反转呢?对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被spring控制,这就叫控制反转(IOC)。IoC在系统运行中时,动态的向某个对象提供它所需要的其他对象。而这就是由我们经常听过的DI来实现的,也就是依赖注入。举个例子,比如对象A需要操作数据库,以前我们总是要在A中自己编写代码来获得一个Connection对象,有了 spring我们就只需要告诉spring,A中需要一个Connection,至于这个Connection怎么构造,何时构造,A不需要知道。在系统运行时,spring会在适当的时候制造一个Connection,然后注射到A中,这样就完成了对各个对象之间关系的控制。A需要依赖 Connection才能正常运行,而这个Connection是由spring注入到A中的,依赖注入的名字就这么来的。那么DI是如何实现的呢? 反射(reflection)就是一种手段,它允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性,spring就是通过反射来实现注入的。

2、Spring框架配置使用流程
(1)启动容器
(2)加载xml配置
(3)根据xml配置路径,执行包扫描
(4)把带注解的对象注册到容器中,一般常用的类注解有这几种:@Controller,@Service,@Configuration,@Repository
(5)装配对象依赖关系,有这两个常见注解:@Autowired,@Resource

3、SpringBoot、SpringMVC和Spring的关系
在学了Spring之后,我们知道了它的概念,那么SpringBoot又和Spring有什么关系呢?它对Spring的缺点进行了改善和优化,典型的特点就是基于“约定优于配置”的思想。它没有了Servlet配置繁琐的缺点,没有代码生成,无需xml文件配置,可以用于嵌入式web服务器、安全、指标、健康监测、外部配置。而SpringMVC呢,它就是一种软件架构模式,把软件系统分为模式、视图、控制器三个基本部分。它的交互流程如下:
在这里插入图片描述
4、项目架构
关于Spring的知识,我会另外写一篇文章进行讲述,今天重点在于博客系统的改造。项目的开发一般的流程如下:业务需求采集->产品设计->系统设计->编码设计->测试->部署上线->运维,那么博客系统架构大概如下:
在这里插入图片描述
对于首页:显示文章列表,有注册/登录功能
对于登录页面:后端,校验用户名,密码成功—创建session,保存用户信息,提供注销功能;
文章详情页面:显示文章信息
个人博客管理页面:和首页类似,显示文章列表,提供新增,删除,修改功能
以上是整体页面的设计;
4.1基础设施搭建
(1)准备环境
maven配置:引入SpringBoot相关依赖,SpringBoot启动配置文件application.properties,里面配置日志打印
(2)基础设施
前后端约定统一的响应格式,统一异常处理,提供自定义异常(抛自定义异常,给用户看的中文错误消息,提高用户体验感)非自定义异常,不能给用户看,需要转成用户可以看的信息,加入统一异常处理后,响应状态码都是200;前后端接口,需要使用的字段,抽象为模型类。统一用户会话管理:使用拦截器,搭配后端服务统一前缀,需要准备好页面以及后端接口;
4.2业务实现
(1)准备页面,在脑海里想象一下,页面之间是如何交互的,前后端又是如何交互的(业务流程)
(2)准备后端接口+模拟数据
特别要注意的是业务流程,如果是ajax相关,ajax发请求–后端请求映射方法,返回响应–ajax回调函数执行,收集请求数据发送。
4.3前端设计
(1)设计html页面元素
结合vue.js前端框架(快速开发前端的js框架,前端dom元素对应的双向绑定的)
绑定变量:
(1.1)标签内容绑定变量:{{变量名}}
(1.2)表单输入绑定:表单标签使用v-model=“变量名”
(1.3)条件:某个标签使用v-if=“变量名”/v-else 使用在某些条件展示内容
变量设计遵循不需要和后端交互原则,如前端按钮点击事件。
(2)设计用户事件相关
在某个事件发生时,绑定事件函数,如页面初始化事件,其他用户事件,一般是和变量结合设计。
4.4前后端开发
(1)约定好前后端接口,前端、后端可以独立开发,符合现在前后端分离的模式。
以上就是我对博客系统的整体架构,下面展示实现流程;
5、后端具体实现
5.1启动类,使用SpringApplication

package org.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication

// 启动类
public class Application {

    //SpringBoot在idea中运行,是在taregt/class目录下

    public static void main(String[] args) {

        // 注意第一个参数是当前类的类对象
        SpringApplication.run(Application.class, args);

    }


}

5.2自定义异常和响应数据的设计

package org.example.exception;


public class AppException extends RuntimeException{

    public AppException(String message) {
        super(message);
    }

    public AppException(String message,Throwable cause) {
        super(message,cause);
    }
}

package org.example.base;


import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@ToString
@Getter
@Setter

public class ResponseResult {

    //业务操作是否成功:前端统一在成功回调处理
    private boolean ok;

    //业务操作成功.
    private Object data;

    //业务操作失败,返回提示信息
    private String message;
}

package org.example.config;

import lombok.extern.slf4j.Slf4j;
import org.example.base.ResponseResult;
import org.example.exception.AppException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 统一异常处理:
 * 响应需要返回统一数据格式(ok=false,messgae)
 * 自定义异常,直接取异常携带中文信息,以便用户读懂
 * 非自定义异常,转换
 */

@ControllerAdvice
@Slf4j
public class ExceptionAdvice {

    @ExceptionHandler(AppException.class)
    @ResponseBody
    public Object handleAppException(AppException e){

        //异常的打印
        log.debug("自定义异常",e);
        ResponseResult json = new ResponseResult();

        json.setMessage(e.getMessage());
        return json;
    }


    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Object handleException(Exception e){

        log.error("系统出错了!",e);
        ResponseResult json = new ResponseResult();

        json.setMessage("未知错误,请联系管理员!");
        return json;

    }
}

5.3用户和文章字段设计

package org.example.model;


import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.io.Serializable;

@ToString
@Setter
@Getter
//User对象需要保存在session里,属于java对象序列化(序列化为二进制数据)
public class User implements Serializable {


    private static final long serialVersionUID = 0;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 昵称
     */

    private String nickname;

    /**
     * 邮箱
     */

    private String email;

    /**
     * 头像
     */

    private String head;


}

package org.example.model;


import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@ToString
@Setter
@Getter
public class Article {

    /**
     * 文章id
     */
    private Integer id;

    /**
     * 文章标题
     */
    private String title;

    /**
     * 文章内容
     */
    private String content;

}

5.4数据的模拟以及对象注册

package org.example.config;

import org.example.model.Article;
import org.example.model.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

@Configuration
public class DataConfig {


    @Bean
    public User user(){
        User user = new User();
        user.setUsername("abc");
        user.setPassword("123");
        user.setNickname("Kaiser");
//        user.setEmail("1732000@qq.com");

        return user;
    }

    @Bean
    public List<Article> articleList(){

        List<Article> list = new ArrayList<>();

        Article a1 = new Article();
        a1.setId(1);
        a1.setTitle("手动蝶阀带我飞");
        a1.setContent("举舞弄轻盈");
        list.add(a1);

        Article a2 = new Article();
        a2.setId(2);
        a2.setTitle("单身宿舍");
        a2.setContent("的单色驱蚊器而且我开发及");
        list.add(a2);

        Article a3 = new Article();
        a3.setId(3);
        a3.setTitle("慕课上");
        a3.setContent("开发,十一哦哦巨大市财政局");
        list.add(a3);

        return list;

    }
}

5.5封装Controller里返回空的情况

package org.example.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.example.config.web.RequestResponseBodyMethodProcessorWrapper;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

//定义SpringMVC的启动配置类
@Configuration
public class AppConfig implements WebMvcConfigurer, InitializingBean {

    @Autowired
    private ObjectMapper objectMapper;

    @Resource
    private RequestMappingHandlerAdapter adapter;

    //之前以@ControllerAdvice+实现ResponseBodyAdvice接口,完成统一处理返回数据包装:无法解决返回值为null需要包装
    //改用现在这种方式,可以解决返回null包装为自定义类型
    @Override
    public void afterPropertiesSet() throws Exception {

        List<HandlerMethodReturnValueHandler> returnValueHandlers = adapter.getReturnValueHandlers();
        List<HandlerMethodReturnValueHandler> handlers = new ArrayList(returnValueHandlers);
        for(int i=0; i<handlers.size(); i++){
            HandlerMethodReturnValueHandler handler = handlers.get(i);
            if(handler instanceof RequestResponseBodyMethodProcessor){
                handlers.set(i, new RequestResponseBodyMethodProcessorWrapper(handler));
            }

        }
        adapter.setReturnValueHandlers(handlers);

    }


    //配置Controller中请求映射方法路径匹配规则
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        //设置路径前缀的规则,以第二个参数的返回值作为请求映射方法是否添加前缀
        configurer.addPathPrefix("api", c->true);

    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

    }

}
package org.example.config.web;

import org.example.base.ResponseResult;
import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;

public class RequestResponseBodyMethodProcessorWrapper implements HandlerMethodReturnValueHandler {

    private final HandlerMethodReturnValueHandler delegate;

    public RequestResponseBodyMethodProcessorWrapper(HandlerMethodReturnValueHandler delegate) {
        this.delegate = delegate;
    }

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return delegate.supportsReturnType(returnType);
    }

    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        //returnValue是Controller请求方法执行完,返回值
        if(!(returnValue instanceof ResponseResult)){//返回值本身就是需要的类型,不进行处理
            ResponseResult json = new ResponseResult();
            json.setOk(true);
            json.setData(returnValue);
            returnValue = json;
        }

        delegate.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
    }
}

5.6后端对于用户登录、注销的响应

package org.example.controller;

import org.example.exception.AppException;
import org.example.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private User user;


    @PostMapping("/login")
    public Object login(@RequestBody User user, HttpServletRequest request){

        //模拟请求账号密码,在数据库查询对比
        if (!this.user.getUsername().equals(user.getUsername())){

            throw new AppException("账号不存在");
        }

        if (!this.user.getPassword().equals(user.getPassword())){

            throw new AppException("密码错误!");

        }

        HttpSession session = request.getSession();
        session.setAttribute("user",this.user);
        return null;

    }

    //注销功能
    @GetMapping("/logout")
    public Object logout(HttpSession session){

        session.removeAttribute("user");
        return null;

    }
}

5.7后端对于文章的新增、查看、修改、删除的响应

package org.example.controller;


import org.example.exception.AppException;
import org.example.model.Article;
import org.example.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.NumberFormat;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/article")
public class ArticleController {

    @Autowired
    private List<Article> articleList;


    @GetMapping("/init")
    public Object init(HttpServletRequest request){
        Map<String,Object> data = new HashMap<>();

        //获取用户信息
        HttpSession session = request.getSession(false);
        if (session != null) {
            User user = (User) session.getAttribute("user");
            if (user != null) {

                data.put("user",user);
            }
        }
        data.put("articleList",articleList);
        return data;//ok=true,data={user:{},articleList:[{}]

    }


    @GetMapping("/detail")
    public Object detail(Integer id){

        for (Article a:articleList){
            if(Integer.compare(a.getId(),id)==0){
                return a;
            }
        }

        throw new AppException("找不到该文章"+id);
    }

    @PostMapping("/add")
    public Object add(@RequestBody Article a){

        int max = 0;
        for (Article cur:articleList){
            if (cur.getId()>max){
                max = cur.getId();

            }

        }
        a.setId(max+1);
        articleList.add(a);
        return null;

    }

    @PostMapping("/update")
    public Object update(@RequestBody Article a){

        for (int i=0;i<articleList.size();i++){

            Article cur = articleList.get(i);

            if (Integer.compare(cur.getId(),a.getId())==0){
                articleList.set(i,a);
                return null;

            }
        }

       throw new AppException("修改失败,没有该文章"+a.getId());

    }


    @GetMapping("/delete")
    public Object delete(int id){

        for (int i=0;i<articleList.size();i++){

            Article cur = articleList.get(i);

            if (cur.getId()==id){

                articleList.remove(id);

                return null;
            }

        }

        throw new AppException("删除失败,没有该文章"+id);

    }

}

6、前端页面的实现
6.1首页

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>YYCN博客</title>

</head>
<body>


<div id="app">
    <h2>YYCN首页</h2>
       <!--    绑定变量 {{...}}-->
    <div v-if="user.nickname">欢迎你,{{user.nickname}} <a href="javascript:void(0)" @click="logout()">注销</a> <a href="personalArticle.html">个人博客管理</a> </div>
    <div v-else> <a href="login.html">登录</a>/<a href="register.html">注册</a></div>

    <ul>
        <li v-for="article in articles">
            <a href="javascript:void(0)" onclick="toDetail(article.id)">{{article.title}}</a>

        </li>

    </ul>

    <!--    以弹出框展示登录页面 展示和隐藏-->
<!--    <button @click="showLogin">登录</button>-->
<!--    <div v-if="showLogin">-->
<!--        用户名:<input type="text">-->


    </div>
    <!--    设计用户事件相关 在某个动作发生时,绑定事件函数   1、页面初始化事件 created:function(){  2、其他用户事件,比如按钮、链接/div元素/图片..点击-->
    <!--    标签上:v-on:click-"方法名" 简写@click="方法名"  定义方法 methods:{ 方法名 function(){  -->
    <!--    涉及后端的数据,需要前后端ajax (1)发送请求 (2)解析响应-->


<script type="application/x-javascript" src="vue.js"></script>
<script type="application/x-javascript" src="axios.js"></script>


<script>
    let app = new Vue({

        el: "#app",
        data: {

            user: {},

            articles: [],

        },
        methods: {

            toDetail: function () {

                localStorage.setItem('aid', id);
                window.location.href = "detail.html";

            },

            logout: function () {

                axios.get("api/user/logout")
                    .then(function (resp) {
                        let body = resp.data;

                        if (body.ok) {

                            app.user = {};

                        } else {
                            alert(body.message)
                        }

                    })
              }
            },

        //初始化方法,页面加载后就执行
        created: function () {
            //发送首页初始化ajax请求,获取用户登录及文章列表信息
            axios.get("api/article/init")
                .then(function (resp) {
                    // console.log(resp)
                    let body = resp.data;

                    if (body.ok) {

                        //如果||前的值等于空,就赋值为||后的值
                        app.user = body.data.user || {};
                        app.articles = body.data.articleList || [];


                    } else {

                        alert(body.message)

                    }
                })

        }


    })
</script>
</body>
</html>

前端页面比较多,这里就不一一上传了,大家可以看我的github账号:github账号
引入SpringBoot框架之后,后端代码少了很多,也少了很多配置,总之引用框架好处太多了,可以跨各种平台部署,灵活的依赖注入,通过POJO缓解了注射测试的数据。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值