我在极速一个月学完黑马的《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<>();//设置idpublic static void setCurrentId(Long id) {threadLocal.set(id);}//获取idpublic static Long getCurrentId() {return threadLocal.get();}//移除idpublic 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/ObjectNameStringBuilder 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的bodyJwtBuilder 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) {// 得到DefaultJwtParserClaims 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 {@Autowiredprivate JwtTokenAdminInterceptor jwtTokenAdminInterceptor;/*** 注册自定义拦截器** @param registry*/protected void addInterceptors(InterceptorRegistry registry) {log.info("开始注册自定义拦截器...");registry.addInterceptor(jwtTokenAdminInterceptor).addPathPatterns("/admin/**").excludePathPatterns("/admin/employee/login");}/*** 通过knife4j生成接口文档* @return*/@Beanpublic 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*/@ExceptionHandlerpublic 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'@ExceptionHandlerpublic 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 }】