java后端正式的企业级项目规范——苍穹外卖篇一

我在极速一个月学完黑马的《java web》课程之后跟着他写了一个java后端项目,但是后面我才发现那只是为了巩固基础的一个简单课程项目,跟实际开发的项目根本不一样。然后后面我暑假去了超星的移动图书馆开发部实习(我主要做前端的),我还是问团队老大哥要了企业的后端项目学习,顺便一起跟着黑马的《苍穹外卖》项目一起学,那么我将讲解两种项目结构,深入了解企业级项目的结构。

《苍穹外卖》项目结构:

超星移动图书馆项目结构(只显示结构,不泄露代码内容):

一、《苍穹外卖》的分三大模块结构

1、回顾基础

那么我们回顾基础,javaweb里讲过一个基础的java项目必须分成三层架构,首先资源数据定义成叫pojo的类(作为接收前端发来的数据、存入数据库、再封装好返回给前端的数据),然后分别是【Controller控制层】、【Service业务层】、【Dao数据层】

【Controller控制层】:有的地方也叫【接入层】,就是生产出对应联通前端的接口,然后接收前端的请求数据,并返回响应数据回前端的

【Service业务层】:根据不同的接口,对应处理具体的业务逻辑

【Dao数据层】:有的地方叫【数据持久层】,就是操作数据库数据的地方,查询或新增修改删除数据

2、分模块

具体为什么呢?他也没细说,我的理解是企业的大型项目时就会需要分模块开发,然后不同的模块可能是不同的人分工写,也或者是不同模块分不同时期阶段编写,同时分模块化的项目也便于维护,分得越细越容易找到哪里出问题。(至于这里苍穹外卖其实并不是一个大型项目,尤其是后台管理系统,只是为了方便大家学习这种模块化开发的理念)

那么这个项目资源可以去黑马那里下载,自行配置,这里不过多叙述,如果想自己照着它配置一个分模块化的项目时,可以参考这个博主的文章,讲得比较详细:《苍穹外卖》知识梳理P1-多模块项目的创建_sky-common-CSDN博客

首先整个项目的父工程项目叫“sky-take-out”,它自身是一个maven管理的父工程项目。sky-take-out中并没有任何内容,只是为了实现统一管理依赖版本,以及聚合其他子模块。

然后就是三大模块:sky-common模块,sky-pojo模块,sky-server模块,这三个模块分别也都是maven管理的工程项目,然后最终都从属于一个父模块:sky-take-out

模块名称模块作用
common子模块,存放公共类,例如:工具类、常量类、异常类等   
pojo子模块,存放实体类、VO、DTO等    
server 子模块,后端服务,存放配置文件、Controller、Service、Mapper等

那么我们可以从上面表格对三个模块的简单解释,可以看出,其实

service模块】是专门对应了【controller、service、mapper】这三层的业务处理;

然后我们之前学的javaweb里有自己手动创建了很多其他的目录,最基础的就是【pojo实体类】,放到【pojo模块】;

然后就是什么【utils工具类】、【config配置类】、【filter过滤器】、【Interceprter拦截器】......一堆杂七杂八的目录,那么这些公用的包目录就可分别放到【common模块】,不是公用的就还是放回【service模块

二、详细解析三大模块内容

这一部分我希望各位直接死记硬背,不要管他为什么这样,逻辑关系啥的,因为他就是一种规范而已,当然我也会简单讲一下为什么吧。

1、pojo模块

这个模块是最简单的,就是一堆实体类而已。

在我们之前学的内容里,为了对应数据库的每一个【表】的数据,我们对应在java也实现类对应的【类】,然后在接收前端接口传过来的数据、响应返回给前端的数据,我们也通常是封装在这些类里,当然也还有针对“分页查询类”、“规范的响应类”这种数据类。

但是在写代码里我们就会发现有的时候前端发来的数据、操作数据库的数据、返回响应的数据并不一致,打个比方:用户注册的业务里,前端可能只需要传“账户名”、“账户密码”、“姓名”;然后操作数据库需要对应生成并传入这个用户的“id”、“账户名”、“账户密码”、“创建时间”、“更新时间”......;最后返回前端的时候只需返回一个规范的Result类,有返回数据要求的就把整个用户信息返回、没有的就不用返回具体数据。

那这样直接用一个 “用户类” 接收、封装这些数据就会很乱,那就要专门再细分这个 “用户类” ,分出三个小类:【dto】、【entity】、【vo】

【dto】

就是专门负责接收前端数据的小类,要在controller层用这个类的参数接收,然后把这个实例化对象信息传给service层

比如下面这个【员工登录dto类】

【entity】

就是对应数据库表的完整的一个类,数据库有的信息它必须都有,然后基本dto、vo的数据他也都有

要在service层接收到controller传过来的dto之后,再次复制封装到【entity】这个完整的类对象里。

最后补充完【entity】里的所有信息,再交给mapper来处理数据库(提示:当然,如果数据库不需要那么完整的数据信息,也可以直接用dto,比如简单的根据id查询)

另外:把dto的值封装进entitty的快捷方法

这里原始的方法是把dto的属性值一个一个给到entity里

那么我们有更快的方法,只要两个类里含有相同的属性,就可以直接用【BeanUtiles】这个工具类的一个方法【copyProperties(val1 , val2)】把val1这个对象里属性的值,赋值给val2对象

【vo】

最后把数据库返回的完整的entity类对象,把有用的需要给前端的数据提取出来,封装到【vo】这个类的实例化对象,装进Result类响应回去。

注意,这里vo跟dto一样,都不是一个完整信息的类,而且vo还会有一些entity里本不应该有的属性信息(比如登录后会返回一个token,但是用户信息里不应该有这个属性),所以不能直接用【BeanUtiles】的【copyProperties(val1 , val2)】直接复制两个对象之间的属性值

然后另外,普通的给类实例化对象设置值,可以一点一点用setter方法

当我们设置这个类的时候给这个类加了【@Build】注解之后

就可以直接【对象.builder()】创建一个builder对象,然后用【.属性()】直接设置属性值了(别忘了最后加一个【.build()】),也很方便。

这两个方法看自己喜欢使用。

另外:可能会有人留意到既然pojo是放【类】的,那【Result类】跟【PageBean类】呢?

这里【Result类】是所有接口的统一返回响应的类,它就只有三个属性:code状态码、msg强求状态信息、data响应数据,而且只负责封装最后的结果,那也就不用分什么dto、vo、entity了;

而【PageBean类】在企业项目我们更喜欢叫【PageResult类】,因为分页查询完的结果究其本质,就还是——Result的data属性值,然后它里面也就:total数据总记录数、records数据列表,而且也只负责封装最后的结果,那也就不用分什么dto、vo、entity了;

所以它两都装到一个叫【result】的包,然后放入归属【common模块】,代表它们是【公用】的【结果类】,Result是任何类型接口最终用到的返回值,PageResult是任何分页查询最终封装的结果值。

 

总结,pojo就是放除了【Result类】跟【PageBean类】的所有类的模块,然后所有类里又分三个小类:【dto】、【entity】、【vo】,【dto】一般是controller跟service方法的接收参数,【entity】是service的操作对象以及mapper的操作对象,【vo】是controller的返回响应数据

2、common模块

这个是个很重要的【公用配置】的模块,很细我将细细分析

整体结构简单来说就是:

名称说明
constant存放相关常量类
context存放上下文类
enumeration项目的枚举类存储
exception存放自定义异常类
json处理json转换的类
properties存放SpringBoot相关的配置属性类
result返回结果类的封装
utils常用工具类

(1)result

前面我在pojo模块已经讲过了,就不再多解释,这里注意展开讲一下【Result】和【PageResult】跟我们之前学的又有什么不同

【PageResult】

没有太多不一样,属性名还是那两,就两个:

1、第二个属性名以前我们是叫“rows”,可能很多地方喜欢叫“records”,“记录数据”,见名知意

2、加一个【implement Serializable】,【Serializable】这个接口是java提供的【序列化接口】,为了让【对象】的数据方便网络传输、并且不会以乱的格式显示在前端那。序列化具体是啥我下面会讲。

源代码(基本通用,可直接cv):


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.List;

/**
 * 封装分页查询结果
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {

    private long total; //总记录数

    private List records; //当前页数据集合

}

【Result类】

以前我们是这么写的

那么现在属性还是这三,方法也还是这三,有2个地方不一样:

1、属性data、方法返回值都是【泛型】;而类也不再是【普通类】,而是【泛型类】

前面我们学过,Result类的data用来接收任意一种类型是数据结果,不过我们之前学的是用【Object】来接收任意类型的数据:

【Object】不就可以接收任意类型了吗?那么为什么这里还要用【泛型】?

1、object类的数据编译器没有类型提示;

2、object类的数据任何类型都可以传,但是泛型一旦定义好了一种类型对象,这个对象就不能乱传别的类型

说的比较抽象,拿大家最耳熟能详的【List<?>】解释吧,List<?>的<>里面就是泛型,我们在定义一个List变量时可以定义里面的成员是任何一种类型:

1、List里的泛型一旦确定了类型,给这个list对象加入值的时候编译器就会有提示

2、并且强制要求按照定义List对象时确定的类型填入数据,乱填其他类型就报错

3、如果是Object类型,那你瞎塞什么数据都行,也不报错,比如下面Object的数组啥类型都能填

语法:

【泛型数据】格式:【泛型标记符  变量】

【泛型方法】格式:【<泛型标记符> 泛型标记符 方法名( )】 ——空参

                                 【<泛型标记符> 泛型标记符 方法名(泛型标记符 形参)】——带参数

 【 泛型类 】 格式:【public class 类名< 泛型标记符 >】

(< 泛型标记符 >这里,【泛型标记符】写别的大写英文字母A-Z 或 “ ? ”也行,只不过有一个约定:一般用E、T、K、N、?,增强代码的可读性,方便团队间的合作开发。)

  • E Element集合元素
  • T Type Java类
  • K Key 键v Value值
  • N Number 数值类型
  • ?表示不确定的Java类型

泛型规定了一种规范、安全性,一旦规定了这个【泛型】具体指什么,你就得按这个标准来填数据

那么我们这里的Result类就得换成泛型,更加的安全、规范,这样一来就能在外面定义了Result类的时候,一旦规定了这个Result类的【T】是什么类型,那后面这个【Result<T>】实例化的对象就只能用定义时规定好的数据!而且会有代码提示!

参考详细文章:https://zhuanlan.zhihu.com/p/331620060

2、第二个不同的地方,跟PageResult一样,要加一个【implement Serializable】,实现【序列化】接口

序列化的好处以及应用场景:
  网络传输:可以将对象通过网络发送到另一个Java虚拟机。(当你想要通过网络发送 Result 对象,比如在Web服务中将API响应发送给客户端。)

  持久化:可以将对象写入磁盘或数据库,并在需要时重新加载。(当你想要将 Result 对象的状态保存到文件或数据库中,以便以后可以恢复它们。)

  分布式系统:在分布式系统中,对象的序列化状态可以被用来在不同的节点之间传递对象。(在分布式系统中工作,需要在不同的服务或组件之间传递 Result 对象。)

那么关于序列化、反序列化的作用解释,我也专门写了一篇文章,还请自行阅读完再回来搭配理解,文章链接:(可以暂时只看到第二大点就够了)java里的序列化反序列化、HttpMessageConverter、Jackson、消息转化器、对象转化器...都是啥?-CSDN博客

Result类源代码:(也可以直接cv,有不同需求再稍微改一下即可,一般所有result类都这样):


import lombok.Data;

import java.io.Serializable;

/**
 * 后端统一返回结果
 * @param <T>
 */
@Data
//1、这里为什么要继承Serializable?
//因为这是为了【序列化】和【反序列化】

//2、这里为什么使用【泛型类】?
//当我们不确定要设置什么类型属性变量的时候、或者不确定设置什么类型的方法的时候,就可以设置泛型类
//这里<T>这个字母不重要,你可以写E、V、M、K都行,只要加了<>都是代表泛型
public class Result<T> implements Serializable {
    private Integer code; //编码:1成功,0和其它数字为失败
    private String msg; //错误信息
    //泛型变量
    private T data; //数据

    //泛型方法
    public static <T> Result<T> success() {
        Result<T> result = new Result<T>();
        result.code = 1;
        return result;
    }

    //泛型方法
    public static <T> Result<T> success(T object) {
        Result<T> result = new Result<T>();
        result.data = object;
        result.code = 1;
        return result;
    }

    //泛型方法
    public static <T> Result<T> error(String msg) {
        Result result = new Result();
        result.msg = msg;
        result.code = 0;
        return result;
    }
}

(2)constant

这个目录存放的就是常量了,在企业项目开发需要用到很多很多常量,我们不能像写一个java文件时那样直接写默认值,否则要更改默认值的时候那么文件里找就会很痛苦。

比如一个已注册用户有两种状态,1代表正常用户,可以使用软件;0表示禁用的用户,可能发了什么不当言论啊、看黄片啊啥的导致他被封了,不能正常使用软件;

那很多地方我们都会对应用户的状态进行判断,如果全都是0、1,那后期我们想把正常状态改成true、禁用状态改成false怎么办,一个一个找着改吗?而且下一个接你代码的人看着一堆0、1肯定懵逼啊?这啥啊?那就要换成【常量】

根据自己、企业具体业务需求写入你的常量文件

(3)context

网上的解说是 “存放上下文”,那具体上下文是啥?以我的理解,这就是前端Vue里面的 “VueX” 或者 uniapp里的 “uni.setStorageSync”,可以保存各种临时数据信息,并处理它们;

比如我们前端开发时用vueX可以在单独某个页面获取到一些获得的数组、列表数据之后,通过vueX保存到整个项目中,然后在别的页面要用的时候取出,还可以进行增删查改;uni.setStorageSync也是,这种业务最常见的就是在登陆的时候,保存【当前登录的用户信息】,然后在其他地方要用【当前用户信息】的时候再取出来用

那么java后端这里也一样,比如最常有的就是【BaseContext】记录登录的用户信息,那么它的原理其实是用【ThreadLocal 线程变量】,在java当前线程里开辟了一个存储空间,然后当多个其他线程需要访问这个变量,就可以来 “共享空间” 共享这个变量了(至于这个ThreadLocal不是重点我不打算讲,有需要自己去看相关知识点:史上最全ThreadLocal 详解(一)-CSDN博客

例子:

在登录时,在JWT令牌校验的时候验证用户正确时,就把【用户当前登录的id】通过BaseContext存入共享变量empId

然后在【新增员工】业务里,service层要根据这个【用户当前登录的id】来设记录这个 【新增了员工信息的 “人是谁” 】,获取这个【用户当前登录的id】的时候就得再通过BaseContext提取共享变量empId的值

BaseContext的源代码(可以cv,根据具体需求修改):


//这个就是可以理解为vue里面的vuex,它通过Thread线程变量,在当前java线程里开辟了存储空间,然后就可以用它共享变量资源了
public class BaseContext {
    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    //设置id
    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    //获取id
    public static Long getCurrentId() {
        return threadLocal.get();
    }

    //移除id
    public static void removeCurrentId() {
        threadLocal.remove();
    }

}

 

 4、enumeration

存放各种枚举类,老实说我不知道有啥用,暂时没用到......

(4)exception

很有用的一个目录,存放各种【自定义异常类型的类

首先我们先回顾一下【全局异常处理器

当我们前端操作错误、后端编译错误、运行超时......等等各种错误导致报错时,java内置的异常信息我们往往是看不懂、也不知道该怎么办的,那就需要我们手动【捕捉异常】

原始的方法是使用【try-catch】,但是【try-catch】是有一个地方要捕捉异常就要写一次,每次都要重复写那么代码,而且相同的异常处理的逻辑可能也是相同的,那就需要像函数、方法那样把这些异常处理的逻辑封装起来。

/

那么我们只需要创建一个【全局异常处理类】,在里面生成【全局异常处理器】,就可以自动捕获到各种异常,将【异常信息】封装到【Result类】的msg里,将【Result类】返回给前端即可。注意,全局异常处理器给前端看得的异常信息就是【Result类的msg】。

那么我们通常是在【service模块】下加一个【handler目录】,里面存放的就是【GlobalExceptionHandler全局异常处理器

全局异常处理器的语法就是在【GlobalExceptionHandler类】加一个【@RestControllerAdvice注解】,然后里面针对不同类型的异常生成不同类型的全局异常处理器方法,在这【每一个方法】上加一个【@ExceptionHandler注解】;最后把【异常类型.getMessage( )】方法获取到的【异常信息Message】封装到Result的msg并输出。

那么我们可以留意到,【GlobalException】里基本的一个【全局异常处理器】里,它的参数也就是捕捉的异常类型,叫【BaseException】。

那么回到【common模块】,在exception目录下的这些【自定义异常类型的类】里,最基础的、必须得有的一个就是【BaseException】,它用于继承java异常错误【Exception类】里最最特殊的【RuntimeException】类的异常。

【RuntimeException】是Exception中的一个子类,是由程序逻辑错误引起的异常情况,即在代码编写过程中产生的错误。它为什么特殊?因为【Exception】中除了他所有子类都必须强制性的做出异常处理,也就是我们常见的“爆红”报错;但是【RuntimeException】不是显性的异常报错,它允许用户自定义对他的异常种类进行处理,处理也行、不处理放着不管也行。

常见的RuntimeException异常包括:

  • NullPointerException:当试图访问一个空对象的成员时抛出。
  • IllegalArgumentException:当传递给方法的参数不合法时抛出。
  • IndexOutOfBoundsException:当尝试访问数组或集合中不存在的元素时抛出。

可能导致RuntimeException异常的情况:

        空指针引用:访问一个未初始化或空对象的成员。

        非法参数:传递给方法的参数不满足预期条件,导致方法无法正确执行。

        索引越界:尝试访问数组或集合中不存在的元素。

        算术错误:例如,除以零或取模运算时的除数为零。

那么【BaseException】继承了【RuntimeException】,他代表【全局异常】,在不确定具体什么异常的情况下都可以用它。而这样一来,也就成为【exception】里其他所有【捕捉异常类】的 “爹”(父类),基本所有错误异常都在它的基础上。

【BaseException】源代码:

继承了【RuntimeException】之后,在它里面写一个【空参构造函数】和一个【有参构造函数】,其中有参构造函数调用父类【RuntimeException】的构造函数,可以根据外界传入的 “字符串参数msg” 给自己的【mssage成员变量】赋值。


/**
 * 业务异常
 */
public class BaseException extends RuntimeException {

    public BaseException() {
    }

    public BaseException(String msg) {
        super(msg);
    }

}

这样一来,外界就可以直接创建【BaseException类】型的实例化对象,根据个人喜好:不处理、或者把【自定义报错信息】传入,都可以。BaseException的【mssage成员变量】值就会变成【自定义报错信息】

最后通过【BaseException实例化对象.getMessage( )】方法获取到BaseException对象的【mssage成员变量

那么细心的朋友就会发现,代码里抛出异常并传入【自定义报错信息】的并不是BaseException啊,你看

但是,它们都无一例外是继承了【BaseException

看到这里,应该很多人已经乱了,能坚持看到这的人都很牛逼了,我当时也是很懵,看着代码跳来跳去研究,终于搞明白了这之间的逻辑,让我用个图画给大家看(可以点击图片放大观看)

其中的本质其实就是:【BaseException】相当于数据类型里的Object,所有类继承于Object,那么所有不同类型的【异常子类】就都继承于【BaseException】;

然后当遇到各种各样的问题的时候,主动throw抛出对应的【异常子类】的实例化对象,并传入对应的【自定义异常信息】参数;

最后每当抛出异常的时候,【全局异常处理器】无需手动调用,自动捕获到异常,并用【BaseException】父类利用 “多态” 来接收各种不同的【异常子类】对象参数,然后通过【.getMessage( )】方法获取【自定义异常信息】,最后通过Result的error( )方法封装到Result的msg里,并输出错误结果。

(5)json

这里放的是用于将【Java对象与json格式之间相互转换】用的【JacksonObjectMapper】

还是一样,我在之前讲序列化的文章里讲过了这部分,具体文章在下面文章的第三大点:java里的序列化反序列化、HttpMessageConverter、Jackson、消息转化器、对象转化器...都是啥?-CSDN博客   

简单来说就是【JacksonObjectMapper】原理就是:

那么这个更不需要认真学,源代码直接cv即可:


import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 */
public class JacksonObjectMapper extends ObjectMapper {

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    //public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

(6)properties

我们之前学javaweb的时候,可能【配置类】都是放在一个叫 “config” 的目录下,但是当时可以留意到这里面有的文件叫 “xxxProperties”,有的叫 “xxxConfig”,【properties】和【config】这两文件其实有不同作用的

所以这里企业项目开发,要把【properties】跟【config】单独分开,其中各种【properties】放到【common模块】的【properties】目录下;【config】放到【service模块】的【config】目录下

那么【properties】跟【config】到底有什么区别?


properties目录下的类通常用于封装外部配置文件中的属性,并提供一种方便的方式来访问这些属性。它们通常与业务逻辑或特定服务无关,而只是提供一些全局配置信息,更侧重于配置属性的读取和封装。

这里我当时其实是忘了之前的基础.......其实我写过关于【properties】的文章,其实他就是 “最便捷的spring boot工程配置” :《后端之路——最规范、便捷的spring boot工程配置_springboot 配置文件规范性-CSDN博客》,

它是为了跟连接数据库、配置端口号...配置的那个【application.yml】文件搭配的,用来方便管 理、配置各种【工具所需要的属性、参数】例如:                                                                         

jwt工具类所需要的 [ 签名密钥名字、有效时间、前端传的令牌名称 ],在【application.yml】文件配置好具体的信息,然后在【properties】对应信息封装好一个类,最后在用到jwt工具类的时候,调用【properties】的实例化对象的属性值,传入给jwt工具类的实例化对象即可。

(点击图片放大看)


config目录下的类则更倾向于定义特定于应用程序某一部分的配置或行为。可能更侧重于配置web层、Redis数据库连接......等等的实际应用和Bean的创建。不需要搭配【application.yml】配置,基本以后都不会变,扫描的时候会自动被执行)                                                                        

比如:                                                                                                                                                

web层配置类

Redis数据库的RedisTemplate对象配置

这些专业知识到后面再讲,这里只需要知道,这些不需要搭配【application.yml】配置,基本以后都不会变,自己本质就是一个【@Bean对象】可以在外面被【@Autowired】注入使用。(当然除了拦截器配置,拦截器配置不用在外面被【@Autowired】注入使用,扫描的时候会自动被执行)  

(7)utils

这个不用我过多解释,很好理解,就是【工具类】,哪里用到它们就调用,常用的工具类就:阿里云oss上传工具、HttpClient后端发送网络请求的接口类、jwt令牌加密解密的工具类、以及一些什么微信支付宝接口类啥的......前三个是最常用的,可以直接cv源代码:

阿里云oss工具类源代码:


import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;

@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

    /**
     * 文件上传
     *
     * @param bytes
     * @param objectName
     * @return
     */
    public String upload(byte[] bytes, String objectName) {

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
            // 创建PutObject请求。
            ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }

        //文件访问路径规则 https://BucketName.Endpoint/ObjectName
        StringBuilder stringBuilder = new StringBuilder("https://");
        stringBuilder
                .append(bucketName)
                .append(".")
                .append(endpoint)
                .append("/")
                .append(objectName);

        log.info("文件上传到:{}", stringBuilder.toString());

        return stringBuilder.toString();
    }
}

HttpClient请求接口(只写了get请求方法,后续需要post之类的接口方法的话还需自己补充):


import com.alibaba.fastjson.JSONObject;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Http工具类
 */
public class HttpClientUtil {

    static final  int TIMEOUT_MSEC = 5 * 1000;

    /**
     * 发送GET方式请求
     * @param url
     * @param paramMap
     * @return
     */
    public static String doGet(String url,Map<String,String> paramMap){
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        String result = "";
        CloseableHttpResponse response = null;

        try{
            URIBuilder builder = new URIBuilder(url);
            if(paramMap != null){
                for (String key : paramMap.keySet()) {
                    builder.addParameter(key,paramMap.get(key));
                }
            }
            URI uri = builder.build();

            //创建GET请求
            HttpGet httpGet = new HttpGet(uri);

            //发送请求
            response = httpClient.execute(httpGet);

            //判断响应状态
            if(response.getStatusLine().getStatusCode() == 200){
                result = EntityUtils.toString(response.getEntity(),"UTF-8");
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                response.close();
                httpClient.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return result;
    }

    /**
     * 发送POST方式请求
     * @param url
     * @param paramMap
     * @return
     * @throws IOException
     */
    public static String doPost(String url, Map<String, String> paramMap) throws IOException {
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        String resultString = "";

        try {
            // 创建Http Post请求
            HttpPost httpPost = new HttpPost(url);

            // 创建参数列表
            if (paramMap != null) {
                List<NameValuePair> paramList = new ArrayList();
                for (Map.Entry<String, String> param : paramMap.entrySet()) {
                    paramList.add(new BasicNameValuePair(param.getKey(), param.getValue()));
                }
                // 模拟表单
                UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
                httpPost.setEntity(entity);
            }

            httpPost.setConfig(builderRequestConfig());

            // 执行http请求
            response = httpClient.execute(httpPost);

            resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
        } catch (Exception e) {
            throw e;
        } finally {
            try {
                response.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return resultString;
    }

    /**
     * 发送POST方式请求
     * @param url
     * @param paramMap
     * @return
     * @throws IOException
     */
    public static String doPost4Json(String url, Map<String, String> paramMap) throws IOException {
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        String resultString = "";

        try {
            // 创建Http Post请求
            HttpPost httpPost = new HttpPost(url);

            if (paramMap != null) {
                //构造json格式数据
                JSONObject jsonObject = new JSONObject();
                for (Map.Entry<String, String> param : paramMap.entrySet()) {
                    jsonObject.put(param.getKey(),param.getValue());
                }
                StringEntity entity = new StringEntity(jsonObject.toString(),"utf-8");
                //设置请求编码
                entity.setContentEncoding("utf-8");
                //设置数据类型
                entity.setContentType("application/json");
                httpPost.setEntity(entity);
            }

            httpPost.setConfig(builderRequestConfig());

            // 执行http请求
            response = httpClient.execute(httpPost);

            resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
        } catch (Exception e) {
            throw e;
        } finally {
            try {
                response.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return resultString;
    }
    private static RequestConfig builderRequestConfig() {
        return RequestConfig.custom()
                .setConnectTimeout(TIMEOUT_MSEC)
                .setConnectionRequestTimeout(TIMEOUT_MSEC)
                .setSocketTimeout(TIMEOUT_MSEC).build();
    }

}

jwt加密解密的工具类:


import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;

public class JwtUtil {
    /**
     * 生成jwt
     * 使用Hs256算法, 私匙使用固定秘钥
     *
     * @param secretKey jwt秘钥
     * @param ttlMillis jwt过期时间(毫秒)
     * @param claims    设置的信息
     * @return
     */
    public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
        // 指定签名的时候使用的签名算法,也就是header那部分
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成JWT的时间
        long expMillis = System.currentTimeMillis() + ttlMillis;
        Date exp = new Date(expMillis);

        // 设置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置过期时间
                .setExpiration(exp);

        return builder.compact();
    }

    /**
     * Token解密
     *
     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
     * @param token     加密后的token
     * @return
     */
    public static Claims parseJWT(String secretKey, String token) {
        // 得到DefaultJwtParser
        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }

}

 3、service模块

终于来到了最重要最重要的一个模块,所有业务逻辑都是在这处理,那么除了controller、service、mapper三层之外,其他的包基本都是与它们直接相关联所需要的东西,或者可以说跟web层相关的东西。

基本的目录结构是这样:

名称说明
config存放配置类
controller存放controller类
interceptor存放拦截器类
mapper存放mapper接口
service存放service类
SkyApplication启动类

这里解释一下web层,其实我们三层架构里【controller、service、dao】是仅针对我们后端spring boot工程开发的一个业务上的三层架构,如果按照整个基于Spring框架的Web应用程序中的角度来看,还应该有个web层(这通常是MVC的框架的内容,了解一下),Web层负责处理用户的请求和响应,以及与模型的交互。

那么以鄙人粗浅的认知解释,【servlet】跟【controller】同在Web层处理外界请求、返回对应响应,但是各自的职责不一样,【servlet】主要是偏向将网络传输间的数据格式转化、拦截过滤请求,是 “城墙的护城河”、“翻译官”;【controller】是 “处理事务、对接信息的外交官”,接收到转好格式后的数据后交给里面的各部门处理,然后返回一个响应数据,最后再经过【servlet】翻译出去。

【servlet】提供底层的一些方便处理web传输交互的一些api,以达到客户端与服务端的联调;【controller】是对底层的抽象化,只专注于上层的具体业务的逻辑。

那么这里有两个类基本要有,一个是【Redis的RedisTemplate配置类】一个是【web层配置类】

(1)config

web层配置类

web层配置类起名可以叫【WebMvcConfiguration】,这里设置的都是【web层的相关配置】,

比如:拦截器类写好之后需要的【注册拦截器配置】来开启拦截器

Swagger自动生成接口文档所需要的【knife4j】的配置

对应将knife4j的Swagger生成的接口文档的静态资源映射(为了让页面渲染显示接口文档数据)

这里提一下一个知识点:

【@Bean】注解,他其实可以等于【@Component】,都是将当前加注解的地方变成一个Bean对象,交给IOC容器管理

为什么用它:【@Bean】的用途更加灵活,当我们引用第三方库中的类需要装配到 Spring 容器时,则只能通过【@Bean】来实现。(比如:利用Swagger生成接口文档页面,就是用到第三方库knife4j来实现,就能用【@Bean】来配置knife4j)

跟component的区别是:

  • 作用域不同

        @Component作用于类,@Bean作用于方法

  • 注册方式不同
    • @Component注解表明一个类会作为组件类,并告知 Spring 要为这个类创建 bean。
    • @Bean注解告诉 Spring 这个方法将会return返回一个对象,这个对象要注册为 Spring 应用上下文中的 bean。方法体中包含了最终产生bean实例的逻辑
  • 使用方式
    • @Component(@Controller、@Service、@Repository)通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中。
    • @Bean一般结合@Configuration一起使用,也可以配置在类的方法中。

【默认情况下,@Bean注解的方法名默认作为对象的名字,也可以用name属性定义对象的名字

那么这里也不用去记,整个WebMvcConfiguration源代码直接cv


import com.sky.interceptor.JwtTokenAdminInterceptor;
import com.sky.json.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

import java.util.List;

/**
 * 配置类,注册web层相关组件
 */
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

    @Autowired
    private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;

    /**
     * 注册自定义拦截器
     *
     * @param registry
     */
    protected void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器...");
        registry.addInterceptor(jwtTokenAdminInterceptor)
                .addPathPatterns("/admin/**")
                .excludePathPatterns("/admin/employee/login");
    }

    /**
     * 通过knife4j生成接口文档
     * @return
     */
    @Bean
    public Docket docket() {
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo)
                .select()
                //下面这里注意!意思是扫描你的controller文件,对应里面的接口代码生成接口文档
                //所以你们要对应自己的项目修改成你们要扫描的controller文件
                //路径一般就是【service模块】的【controller目录】
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }


    /**
     * 设置静态资源映射
     * @param registry
     */
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

    /**
     * 扩展Spring MVC框架的消息转换器
     * @param converters
     */
    //List<HttpMessageConverter<?>>这个是一个装有很多消息转换器的一个大容器里,里面可以放各种我们自定义的格式的转换器
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters){
        //创建一个消息转换器对象
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        //需要为消息转换器设置一个对象转换器,对象转换器可以将java对象序列化为json
        //传入的参数是我们自定义的一个叫【JacksonObjectMapper】的类,根据我们的需要进行的序列化消息转换器
        converter.setObjectMapper(new JacksonObjectMapper());
        //然后将这个我们新改好的序列化器,加入到这个List<HttpMessageConverter<?>>消息转化器大容器中
        //然后因为add方法并不会把这个自定义转换器放到第一位,只会默认放最后,那我们要用它就得放第一位,那就多加一个参数0,意思是放到第0位
        converters.add(0,converter);
    }
}

其中Swagger接口文档还需要在【整个父工程】的【pom.xml】配置【knife4j库】的依赖

<dependency>
     <groupId>com.github.xiaoymin</groupId>
     <artifactId>knife4j-spring-boot-starter</artifactId>
     <version>3.0.2</version>
</dependency>

怎么使用以后再说

Redis的RedisTemplate

先不说,因为这里好没讲到Redis,看我以后的文章会讲。

(2)controller、service、mapper、intercepter

这几个为什么放一块,因为以及熟悉得不能再熟悉了,学完javaweb的都应该知道这四个是啥了,三层架构加拦截器嘛......

(3)handler

基本有一个GolobalExceptionHandler就够了,这个目录就是一个放全局异常捕获器的目录。

这些全局捕获器可以自动捕捉到报错,然后根据自己的喜好编写报错时要错的事逻辑

源代码:(后面需别的异常需要捕捉,在自己补充)


import com.sky.constant.MessageConstant;
import com.sky.exception.BaseException;
import com.sky.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.sql.SQLIntegrityConstraintViolationException;

/**
 * 全局异常处理器,处理项目中抛出的业务异常
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 捕获业务异常
     * @param ex
     * @return
     */
    @ExceptionHandler
    public Result exceptionHandler(BaseException ex){
        log.error("异常信息:{}", ex.getMessage());
        return Result.error(ex.getMessage());
    }

    //追加一个全局异常处理器,当新增员工时有相同用户名的,数据库因为设置了唯一,sql会下面这个报错
    //java.sql.SQLIntegrityConstraintViolationException:
    //Duplicate entry 'shadan' for key 'employee.idx_username'
    @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
        log.error("sql异常信息:{}",ex.getMessage());

        //因为这个异常信息有很多种,要判断是不是我们要的下面这个异常,就要先看有没有“Duplicate entry”
        //Duplicate entry 'shadan' for key 'employee.idx_username'
        String message = ex.getMessage();
        if(message.contains("Duplicate entry")){
            String username = message.split(" ")[2];
            //尽量不用我们自己写的报错信息,尽量传入【常量】
            //String msg = username + "已存在,请勿重复添加同一身份信息";
            String msg = username + MessageConstant.ALREADY_EXISTS;
            return Result.error(msg);
        }
        //尽量用常量
        //return Result.error("未知错误");
        return Result.error(MessageConstant.UNKNOWN_ERROR);
    }
}

(4)xxxApplication

xxx是你的项目名字,通常这样命名xxxApplication,这就是你整个项目的启动类,加上【@SpringBootApplication】和【@EnableTransctionManagement】才可以变成启动类,整个项目就以他为入口

 这个应该没有需要源代码的吧...除非没学过javaweb直接跳到这了,算了放着吧


import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
public class SkyApplication {
    public static void main(String[] args) {
        SpringApplication.run(SkyApplication.class, args);
        log.info("server started");
    }
}

(5)最后一个,resourse目录

都知道resourse是放静态资源的地方,那么我们一般就放【动态sql—mapper包】、【application.yml】、【application-dev.yml】

【动态sql—mapper包】没什么好说的,就是xml文件的mapper层文件,可以动态操作sql

那为啥会有两个yml配置文件??

因为写过后端项目的朋友一定会有这样的经历,如果你要将这个项目在自己的本地环境运行、然后通过局域网跟别人联调、然后放到服务器联调,那么数据库、网络监听的地址、端口号这些肯定都是不一样的,你需要重复“N的N次方次”、“一万一亿次”的“无限循环”地进行这些配置数据的修改。

那么在公司是绝对不允许这么低效率开发的,因为企业开发规定了开发有三个阶段:【开发环境dev】、【测试环境test】、【生产环境prod】

【开发环境dev】就是你再写代码的时候用到的mysql、redis数据库或者端口号之类的配置

【测试环境test】开发完了交给测试人员,测试人员用他电脑里的配置

【生产环境prod】交付了投入市场了,那最终用的是什么服务器公网、端口、数据库...

那么我这里个人开发学习没那么复杂,就先创建一个【开发环境dev】的【aaplication-dev.yml】,然后在里面写上真实的数据值

然后在基础的真正的那个【application.yml】里,首先在spring.profiles.active指定要用的是哪个环境,注意对应环境的yml文件必须得写成【application-xxx.yml】,这样才能直接根据【xxx来查找定位到是哪一个环境的yml文件。

然后通过【变量】的形式来引入【aaplication-dev.yml】的真实数据值,使用的语法:【${ xxx.xxx.xxx }】

自此,暂时讲完《苍穹外卖》的这种项目结构,下一篇讲超星移动图书馆的另一种项目结构

你可以按照以下步骤来部署后端项目到 Nginx 上: 1. 安装 Nginx:首先,确保你的服务器上已经安装了 Nginx。你可以根据你的操作系统的不同,使用相应的包管理器来安装。 2. 配置 Nginx:打开 Nginx 的配置文件,通常位于 `/etc/nginx/nginx.conf` 或者 `/etc/nginx/conf.d/default.conf`。在 `http` 部分添加一个新的 server 块,用于配置你的后端项目。 例如,假设你的后端项目运行在本地的 8000 端口上,你可以添加以下配置: ``` server { listen 80; server_name example.com; # 将 example.com 替换为你的域名或 IP 地址 location / { proxy_pass http://localhost:8000; # 将 localhost:8000 替换为你的后端项目地址 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } ``` 保存并关闭配置文件。 3. 测试配置文件:为了确保配置文件没有错误,你可以使用以下命令来测试: ``` nginx -t ``` 如果没有错误提示,说明配置文件正确。 4. 启动 Nginx:使用以下命令启动 Nginx 服务: ``` service nginx start ``` 或者 ``` systemctl start nginx ``` 确保没有错误提示。 5. 访问后端项目:现在,你可以通过在浏览器中输入你的域名或 IP 地址来访问你的后端项目了。 注意:上述配置仅仅是一个简单的示例,你可能需要根据你的具体需求进行修改。另外,如果你的后端项目是运行在其他服务器上,你需要将 `proxy_pass` 中的地址修改为正确的后端项目地址。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值