SpringBoot项目:个人博客的搭建(二)-开发

个人博客的搭建(一)-项目准备

准备

有了前期的准备做铺路,就可以开始我们的编码路程了

命名约定

开始前,先对service/dao层的方法名进行约定统一

  • 获取当个对象的方法用getOne()
  • 获取全部对象用getAll()
  • 获取多个对象用get()
  • 插入的方法用save做前缀
  • 修改用update做前缀
  • 删除用remove做前缀
  • 通过除主键外获取对象用getXXXByXX()
  • 统计方法用count()

配置文件

这里可以创建多两份配置文件application.yml,一份用于开发环境,一份用于生成环境

开发环境dev的配置(application-dev.yml):

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/blog?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai
    username: 你的数据库名称
    password: 你的数据库密码
logging:
  level:
    cn.zpeace.blog.mapper: debug  #用于打印sql语句
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true  #开启驼峰命名匹配
  mapper-locations: classpath*:/mapper/**/*.xml  #扫描mapper.xml文件路径

生产环境rpo的配置(application-pro.yml):

logging:
  file: log/blog-dev.log  #主要修改日志配置,生产环境日志输出到文件中,并且不打印sql语句

然后可以在application.yml中指定激活环境:

spring:
  profiles:
    active: dev  #指定激活环境

也可以在运行的时候通过命令行来指定激活环境

java -jar 你的项目包名称 --spring.profiles.active=pro

持久层框架

这里我使用了mybatis-plus作为项目的持久层框架,选择JPA或者mybatis都是一样的,都可以实现这个博客项目,只是具体的开发操作不同而已。

选择mybatis-plus主要是可以简化持久层的开发,mybatis-plus默认已经封装了一些常用的CRUD操作,可以通过继承BaseMapper来使用,不选择JPA的原因呢,是因为JPA生成的SQL语句很难看,不方便观察

public interface BlogMapper extends BaseMapper{
}

具体使用可以查看官方文档,MyBatis-Plus

mybatis-plus的配置

@Configuration
@EnableTransactionManagement  //代表开启分页
public class MybatisPlusConfig {
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
        // paginationInterceptor.setOverflow(false);
        // 设置最大单页限制数量,默认 500 条,-1 不受限制
        // paginationInterceptor.setLimit(500);
        return paginationInterceptor;
    }
}

并且在springboot启动类上标注@MapperScan来扫描mapper类的包路径

@SpringBootApplication
@MapperScan("cn.zpeace.blog.mapper")
public class BlogApplication {

    public static void main(String[] args) {
        SpringApplication.run(BlogApplication.class, args);
    }

}

分模块开发

日志记录模块

这里就简单对前台被访问时进行记录

首先,创建一个切面类

@Aspect  //表面这是一个切面类
@Component  //注入到spring中
@Slf4j //获得日志对象
public class logAspect {
    
    //定义一个切入点,拦截controller包下的类的方法
    @Pointcut("execution(* cn.zpeace.blog.controller.*.*(..))") 
    public void log(){
    }

    @Before("log()")
    public void doBefore(JoinPoint joinPoint){
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest req = attributes.getRequest();

        String ip = MyBlogUtil.getRemortIP(req);
        String url = req.getRequestURL().toString();


        //获得被拦截的方法名
        String proxyMethod = joinPoint.getSignature().getDeclaringTypeName()  + "." + joinPoint.getSignature().getName();
        //获得输入
        Object[] args = joinPoint.getArgs();

        //记录日志
        log.info("访问者IP:{} ,请求的URL:{} ,调用的方法:{} ,方法参数:{}",ip,url,proxyMethod,args);

    }
    
}

异常处理模块

存在异常:资源找不到异常。

定义一个查询结果不存在异常,当查询结果为null时,返回404状态码给客户端

@ResponseStatus(HttpStatus.NOT_FOUND) //表明发生该异常时,将会返回404状态码给客户端
public class NotFoundException extends RuntimeException {
    public NotFoundException() {
    }

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

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

    public NotFoundException(Throwable cause) {
        super(cause);
    }

    public NotFoundException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

同时定义一个异常拦截器

@Slf4j
@ControllerAdvice //所有的异常都通过这个拦截器来进行重新配置
public class ExceptionHandler {

    //当抛出Exception类及其子类异常的时候,进行拦截
    @org.springframework.web.bind.annotation.ExceptionHandler(Exception.class)  
    public ModelAndView handlerException(HttpServletRequest request,Exception e) throws Exception {
        log.error("Request URL:{} ,exception {}",request.getRequestURL(),e);

        if (AnnotationUtils.findAnnotation(e.getClass(),
                ResponseStatus.class) != null) {
            throw e;
        }
        ModelAndView mav = new ModelAndView();
        mav.addObject("url", request.getRequestURL());
        mav.addObject("exception", e);
        mav.setViewName("error/error");
        return mav;
    }
}

分类模块

根据前面的需求,对于分类功能,大概需要提供这几个方法:

public interface CategoryService {

    Integer savaOne(Category category); //增加一个分类

    Integer updateOne(Category category); //修改一个分类

    Integer removeOne(Integer categoryId); //删除一个分类

    Category getOne(Integer categoryId);  //通过id获取一个分类

    //IPage 是mybatis-plus封装的一个分页对象,可以简化分页操作
    IPage getAll(Integer pageNum, Integer pageSize);  //分类展示分类

    List  getAll(); //获取所有分类

    Integer count();  //统计分类个数
}

标签模块

标签跟分类差不多一样,需要注意的是,删除一个标签的时候,应该同时把关联表中该标签的行也删除了

public interface TagService {

    Integer saveOne(Tag tag);

    Integer update(Tag tag);

    Integer removeOne(Integer tagId);

    Tag getOne(Integer tagId);

    IPage getAll(Integer pageNum,Integer pageSize);

    List getAll();

    Integer count();

}

友链模块

public interface LinkService {

    Integer saveOne(Link link);

    Integer updateOne(Link link);

    Integer removeOne(Integer linkId);

    IPage getAll(Integer pageNum, Integer pageSize);

    List getAll();

}

用户模块

public interface UserService {

    User findUser(String username,String password) throws Exception;// 管理员登录查询

    User getUserById(Integer userId);  //通过Id获取管理员信息

    Integer updateUserInfo(User user);  //更新管理员的信息

    User checkPassword(Integer userId,String password); //判断输入的密码与管理员id是否匹配

    Integer updatePassword(Integer userId,String password);//更新管理员密码

    String getIntroduceAndConvert(Integer userId); //获取关于我

    Boolean isUserExist(String username); //通过用户名判断用户存不存在
}

登录与拦截模块

登录逻辑处理

@PostMapping("/login")
public String login(String username, String password, HttpSession session, Model model) throws Exception {

    log.info("管理员尝试进行登录,用户名为:{} ,密码为:{}", username, password);

    Boolean userExist = userService.isUserExist(username);
    if (userExist) {
        User user = userService.findUser(username, password);
        if (user != null) {
            session.setAttribute("user", user);
            log.info("登录成功,用户名为:{}", user.getUsername());
            return "redirect:/admin/";
        } else {
            log.info("登录失败,失败原因:用户名或者密码错误");
            model.addAttribute("message","用户名或者密码错误");
            return "admin/login";
        }
    } else {
    log.info("登录失败,失败原因:用户不存在");
    model.addAttribute("message","用户不存在");
    return "admin/login";
    }
}

配置一个登录拦截器,编写preHandle方法

public class LoginHandler implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object user = request.getSession().getAttribute("user");
        if (user == null) {
            //未登录
            request.setAttribute("message","没有权限,请先登录");
            request.getRequestDispatcher("/admin/login").forward(request,response);
            return false;
        }else {
            return true;
        }
    }
}

将登录拦截器添加到容器中

@Configuration
public class MyWebConfig {

    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        WebMvcConfigurer configurer = new WebMvcConfigurer() {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                registry.addInterceptor(new LoginHandler())
                        .addPathPatterns("/admin/**") //拦截所有/admin 请求
                        .excludePathPatterns("/admin/login");//排除掉登录请求
            }
        };
        return configurer;
    }
}

博文模块

public interface BlogService {

    Integer savaOne(Blog blog); //保存一个博文

    Integer updateOne(Blog blog); //修改一个博文

    Integer removeOne(Integer blogId); //删除一个博文

    Blog getOne(Integer blogId);  //获得一个博文详情

    Blog getOneAndConvert(Integer blogId); //获得一个博文,并把markdown格式转换为html格式

    Integer updateView(Integer blogId,Integer blogView); //更新博文的浏览量

    Map> getArchives(); //获得归档

    Integer count(); //博文统计

    //用于展示在标签单项下的博文
    IPage getByTag(Integer pageNum,Integer pageSize,Integer tagId);//通过tag获取博文

    /**
     * @param pageNum 当前页
     * @param pageSize 当前的条目
     * @param keyword  搜索关键词
     * @param categoryId 分类Id
     * @param published 是否发布
     * @return
     */
    MyPage getAll(Integer pageNum, Integer pageSize, String keyword , Integer categoryId , Boolean published); 

}

MyPage为本人自己用来封装分页查询结果的对象。

为什么不用IPage?因为通过拦截器进行的拦截分页,会自动添加limit在sql语句最后端,这样会造成的结果是

  • 普通的分页查询没有什么问题
  • 用于多对多,或者多对一的查询,因为映射关系,mybatis会自动帮忙把查询语句的结果封装在对象中,最后就会造成数据库查出5条数据,然后被封装后的数据不到5个的情况。
  • 这种问题网上还有另外的解决方案,就是通过嵌套查询,不过这样会造成N+1问题。最好还是自己实现分页,把limit语句写在主查询中就可以完美解决问题

具体实现

前面的分类、标签模块因为实现方法都比较简单,所以没有把实现贴出来。

这里贴一下博文操作的一些实现。

保存博文
@Override
public Integer savaOne(Blog blog) {
    /*
    将博文简介转化为html格式
    把html格式的简介存储到数据库,主要是出于,博文简介是展示在主页上的 ,而主页一般是展示博文列表
    如果在取出来的时候在转化,访问首页的时候,会因为后端正在循环转化,页面的响应将会变慢
    */
    String blogBrief = MarkdownUtil.markdownToHtmlExtensions(blog.getBlogBrief());

    //将html格式的保存进blog对象
    blog.setBlogBrief(blogBrief);

    blogMapper.insert(blog);  //保存到数据库


    //保存完博文后,需要同时把关联的标签id  写到数据库的关系表中
    return blogMapper.insertAssociation(blog);
}

修改博文
@Override
public Integer updateOne(Blog blog) {

    blogMapper.deleteAssociationById(blog.getBlogId());  //删除文章与标签之间的关联

    blogMapper.insertAssociation(blog); //插入新的 文章与标签之前的关联

    //转化博文简介
    blog.setBlogBrief(MarkdownUtil.markdownToHtmlExtensions(blog.getBlogBrief()));

    blog.setUpdateTime(new Date());  //设置更新时间,出于博文会更新访问量的考虑,没有把blog表中的update_time 设置为根据当前时间更新,使用这里要自己设置更新时间

    return blogMapper.updateById(blog); //更新博文
}
删除博文
@Override
public Integer removeOne(Integer blogId) {
    blogMapper.deleteAssociationById(blogId); //删除关联关系
    return blogMapper.deleteById(blogId); //删除博文
}
获取一个博文:

主要用在编辑博文上

@Override
public Blog getOne(Integer blogId) {

    return blogMapper.getOne(blogId);  
}
获取一个博文并且转化格式

这个方法主要用在了前台文章详情页面,把markdown格式转化为html格式,便于浏览

@Override
public Blog getOneAndConvert(Integer blogId) {
    Blog blog = blogMapper.getOne(blogId);
    if (blog == null) {
        throw new NotFoundException("该博文不存在");
    }
    blog.setBlogContent(MarkdownUtil.markdownToHtmlExtensions(blog.getBlogContent()));
    return blog;
}
获取按年份归档的博文

具体思路:

  1. 可以先查询博客的创建时间,用数据库的year()函数获得年份,然后再去重,就获得了博文所有存在的年份了
  2. 创建一个map,把年份作为key,遍历这个map ,把key作为查询条件,查询数据库,并把查询结果作为value
@Override
public Map> getArchives() {
    List years = blogMapper.getYear();
    Map> map = new HashMap<>();

    for (String year : years) {
        map.put(year,blogMapper.getBlogByYear(year));
    }
    return map;
}
通过标签获取博文
@Override
public IPage getByTag(Integer pageNum, Integer pageSize, Integer tagId) {
    //通过关联关系,获得与该标签关联的博文id是哪些
    List blogIds = blogMapper.getBlogByAssociationTagId(tagId);

    //如果没有关联就返回null
    if (blogIds == null || blogIds.size() <= 0) {
        return null;
    }

    //通过博文id进行查找 并返回结果
    return blogMapper.selectPage(new Page<>(pageNum,pageSize),new QueryWrapper()                      .orderByDesc("create_time").in("blog_id",blogIds).eq("published",true));
    
}

分页获取博文
@Override
public MyPage getAll(Integer pageNum, Integer pageSize, String keyword , Integer categoryId , Boolean published) {


    //设置页码,展示数量,总数
    MyPage page = new MyPage<>(pageNum,pageSize,blogMapper.count(keyword,categoryId,published));

    //通过不同的条件查询博文
    List blogs = blogMapper.getBlogDetail(page, keyword,categoryId,published);

    //把博文设置到page对象中
    page.setRecords(blogs);

    return page;
}

关于DAO层的编写

Dao的编写主要是写sql语句,这里贴一下一些比较复杂的sql语句,主要是BlogMapper中getOne()、getBlogDetail()、count()比较复杂一点

  1. 首先设置一个用来封装关联查询结果的resultMap

    
        
        
        
        
        
        
        
        
        
        
        
        
        
            
            
        
        
            
            
        
    
    
    
  2. 然后定义sql查询语句

    • getOne方法

      通过级联查询,获取到t_blog表的所有字段、t_category表中的category_name字段,t_tag表中的tag_name字段

    
    
    
    • getBlogDetail()

      通过不同的条件查询博文列表

    
    
    
    • count()

      通过不同的条件查询出博文数

    
    
    

功能完善

markdown图片上传

1570625271513.png

集成的markdown编辑器,上传图片需要自己在后端进行处理,具体上传的后端处理路径可以在初始化markdown编辑器的时候指定

var contentEditor = editormd("md-content", {
    width: "100%",
    height: 640,
    syncScrolling: "single",
    path: "/plugin/editormd/lib/",
    toolbarModes: 'full',
    saveHTMLToTextarea:true,
    /**图片上传配置*/
    imageUpload    : true,
    imageFormats   : ["jpg", "jpeg", "gif", "png", "bmp", "webp"], //图片上传格式
    imageUploadURL: "/admin/blog/md/uploadimg",
    onload: function (obj) { //上传成功之后的回调
    }
});

对editor.md编辑器的上传图片后端处理,主要就是把文件保存到服务器,然后回显一个URL给前台,让前台可以通过这个URL访问到这张图片,这个URL可以通过浏览器直接输入地址来访问检验有没有问题。

这里建议再开一个服务器,把图片都保存在这个服务器。

我这里就采用nginx来保存上传的图片,最后回显url的时候把服务器地址写对就可以了,比较简单。

@ResponseBody
@PostMapping("/blog/md/uploadimg")
public Map mdUpload(@RequestParam(value = "editormd-image-file",required = false) MultipartFile file,
                                   HttpServletRequest request,
                                   HttpServletResponse response){

    Map result = new HashMap();


    String fileName = file.getOriginalFilename();//文件名
    String newFileName = MyBlogUtil.getNewFileName(fileName);//设置文件新名称

    //用户文件存储的路径
    String userPath = MyBlogUtil.getUserDirectory(request.getSession());


    //博客图片保存目录
    String blogPath = parentDirPath + userPath + "blog" + File.separator + "img";

    //获得最终保存的目录
    File blogDir = new File(blogPath);
    if (!blogDir.exists()){
        blogDir.mkdirs();
    }

    //最终文件上传的路径
    File filePath = new File(blogPath + File.separator + newFileName);

    String fileUrl = fileSever + userPath.replace("\\","/") + "blog/img/" + newFileName;

    try {
        request.setCharacterEncoding("utf-8");
        response.setHeader("Content-Type", "text/html");
        file.transferTo(filePath);

        result.put("success",1);
        result.put("message","上传成功");
        result.put("url",fileUrl);
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    } catch (IOException e) {
        result.put("success",0);
        e.printStackTrace();

    }


    return result;
}

上面有两个变量fileSever,parentDirPath,这里可以根据自己的具体情况如何在配置文件配置

这个可以等到后面,我们配置完nginx服务器的时候再来指定

@Value("${fileSever}")
private String fileSever;  //代表图片上传到的服务器域名

@Value("${parentDirPath}")
private String parentDirPath;  //代表图片上传到的父目录名称 ,也就是图片服务器映射的根目录

修改管理员信息

主要是修改头像的功能实现

  1. 使用MultipartFile对象来接受前台传过来的图片数据
  2. 重命名保存到nginx服务器中
  3. 把URL地址设置到user对象中
  4. 最后更新管理员信息
@PostMapping("/user")
public String updateInfo(User user, @RequestParam(value = "avatar-img",required = false)MultipartFile file,
                         HttpSession session){

    //判断上传的图片是否为空
    if (!file.isEmpty()) {
        //用户目录
        String userDirectory = MyBlogUtil.getUserDirectory(session);

        //文件重命名
        String newFileName = MyBlogUtil.getNewFileName(file.getOriginalFilename());
        log.info("用户目录为{},文件名为{}",userDirectory,newFileName);

        //设置头像的保存位置
        String avatarPath = parentDirPath + userDirectory + "avatar" + File.separator;
        File avatarDir = new File(avatarPath);

        //保存用户头像的目录如果不存在就创建
        if (!avatarDir.exists()){
            avatarDir.mkdirs();
        }
        File avatarFile = new File(avatarPath + newFileName);
        try {
            //保存图片
            file.transferTo(avatarFile);
        } catch (IOException e) {
            e.printStackTrace();
        }

        String avatorUrl =fileSever + userDirectory.replace("\\","/")+ "avatar/" + newFileName;

        user.setAvatar(avatorUrl);
    }


    userService.updateUserInfo(user);

    return "redirect:/admin/user";
}

Nginx

首先先去nginx官网下载: http://nginx.org/

下载解压完后双击nginx.exe ,看到界面一闪就说明已经启动了

也可以通过cmd命令start nginx 来启动(需要先进入到nginx的目录)

修改了nginx的配置文件后,如果nginx正运行着,记得要用nginx -s reload 重载一下。

反向代理

location / {
            proxy_pass http://127.0.0.1:8080;  #这里写tomcat的服务器地址
            
            proxy_set_header   Host             $host;
            proxy_set_header   X-Real-IP        $remote_addr;                       
            proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;

        }

反向代理只要配置proxy_pass 就够了,配置完反向代理后有个问题就是我们通过request.getRemoteAddr()获取

到的访问ip地址都是nginx服务器的地址,无法获得访客的ip地址,因为所有的请求都是经过nginx转发给我们的。

可以通过设置这三行,意思是nginx把请求转发给我们的tomcat的时候,给request请求增加这3个头部。

在我们程序里就可以通过这行代码来获取到访客的ip地址

request.getHeader("X-Real-IP")

动静分离

开发环境nginx的配置

location ~\.(css|js|png|jpg|git|jpeg|bmp|webp)$ {
            root D:/nginx-dir/blog/static;
        }

表明遇到请求css/js/png等静态资源时,直接去D:/nginx-dir/blog/static目录下查找,这样就不会去我们的tomcat上查找了,只有当访问动态的资源时,nginx无法处理,才会把请求转发给tomcat。

简单配置完nginx后,就可以配置项目中的图片上传的位置

dev.yml

fileSever: http://localhost/  #指定nginx服务器的地址
parentDirPath: D:\nginx-dir\blog\static\ #指定nginx映射的目录,也就是nginx配置文件root配置的目录

生成环境nginx的配置

在CentOS 7 上可以直接通过 yum install nginx 进行安装

Linux上跟windows上的配置大同小异,主要是路径分割符不一样

location / {
        proxy_pass  http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header REMOTE-HOST $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

    location ~\.(css|js|png|jpg|git|jpeg|bmp|webp)$ {
        root    /root/blog/static;
    }


pro.yml文件配置

fileSever: http://127.0.0.1/
parentDirPath: /root/blog/static/

开发大致就介绍这些

你可能感兴趣的:(SpringBoot项目:个人博客的搭建(二)-开发)