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<>();//设置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 }】

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/400519.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深度优化Nginx负载均衡策略,携手Keepalived打造高可用服务架构新纪元

作者简介&#xff1a;我是团团儿&#xff0c;是一名专注于云计算领域的专业创作者&#xff0c;感谢大家的关注 座右铭&#xff1a; 云端筑梦&#xff0c;数据为翼&#xff0c;探索无限可能&#xff0c;引领云计算新纪元 个人主页&#xff1a;团儿.-CSDN博客 目录 前言&#…

【JavaEE初阶】文件操作和IO

目录 &#x1f334;认识文件 &#x1f6a9;树型结构组织和目录 &#x1f6a9;文件路径&#xff08;Path&#xff09; &#x1f6a9; 文件分类 &#x1f38d;Java 中操作文件 &#x1f6a9; File 概述&#xff1a; &#x1f4cc;属性 &#x1f4cc;构造方法 &#x1f4c…

企业大模型业务架构技术选型分析

AI赋能企业&#xff1a;选择适合你的大模型业务架构 现代企业中&#xff0c;大模型业务日益普及&#xff0c;主要涵盖AI Embedded、AI Copilot和AI Agent三大架构。本文深入剖析其特性与适用场景&#xff0c;为企业选择合适的大模型业务架构提供指导&#xff0c;助力企业高效应…

Spring容器启动的过程(main)

大体流程如下 1、初始化 首先&#xff0c;Spring会通过用户提供的配置信息&#xff08;例如XML文件或者注解&#xff09;来初始化一个BeanFactory&#xff0c;这个BeanFactory是Spring容器的核心&#xff0c;它负责创建和管理所有的Bean。 2、读取配置生成并注册BeanDefini…

开源一套金融大模型插件(ChatGPT)

shares vscode 插件A 股量化交易系统自研金融大模型&#xff0c;复利Chat 源码地址&#xff1a; https://github.com/xxjwxc/shares

面试题:Rabbitmq怎么保证消息的可靠性?

1.消费端消息可靠性保证&#xff1a; 消息确认&#xff08;Acknowledgements&#xff09;&#xff1a;(自动(默认),手动) 消费者在接收到消息后&#xff0c;默认情况下RabbitMQ会自动确认消息&#xff08;autoAcktrue&#xff09;。为保证消息可靠性&#xff0c;可以设置auto…

CentOS 7设置静态IP地址的详细指南

CentOS 7设置静态IP地址的详细指南 配置静态IP地址是服务器或虚拟机管理的重要步骤之一&#xff0c;特别是在需要稳定、可预测的网络环境时。本文将详细介绍如何在CentOS 7上设置静态IP地址&#xff0c;帮助确保你的系统网络配置符合需求。 1. 查看当前网络配置 在进行任何更…

文件长度超出芯片容量, 超出部份将被忽略!ch341a编程器报错解决方法

出现这个错误提示&#xff0c;说明你正在刷的是华硕主板的cap格式BIOS文件。 编程器不支持这种文件&#xff0c;需要转换成编程器专用版本BIOS文件。 华硕cap格式BIOS转编程器bios文件&#xff0c;转换工具下载地址&#xff1a;https://download.csdn.net/download/baiseled/88…

再见Figma!!新的设计,代码协作神器!【送源码】

软件介绍 Penpot 是一款专门用来帮助设计师和开发者更好地合作的软件。它可以让设计师轻松地做出漂亮的设计稿&#xff0c;还能让这些设计稿变成真正的网站或者应用的一部分。这样&#xff0c;设计师和开发者之间就不会因为沟通不畅而产生麻烦了。 Penpot 专为设计师与开发者之…

在docker中进行日志切割

先在Linux中安装docker&#xff0c;然后在docker中安装appnode面板&#xff0c;并进行docker网络端口映射。接着进入docker&#xff0c;进行nginx日志切割。 安装docker 第一步&#xff0c;卸载旧版本docker。 若系统中已安装旧版本docker&#xff0c;则需要卸载旧版本docke…

书生大模型实战营-基础关-XTuner 微调个人小助手认知

XTuner 微调个人小助手认知 环境配置模型效果预览微调数据准备微调配置微调训练权重格式转换模型合并页面对话 环境配置 # 创建虚拟环境 conda create -n xtuner0812 python3.10 -y# 激活虚拟环境&#xff08;注意&#xff1a;后续的所有操作都需要在这个虚拟环境中进行&#…

Docker搭建Minio容器

Docker搭建Minio容器 前言 在上一集我们介绍了分布式文件存储行业解决方案以及技术选型。最终我们决定选用Minio作为分布式文件存储。 那么这集我们就在Docker上搭建Minio容器即可。 Docker搭建Minio容器步骤 创建Minio文件目录 我们选择创建/minio/data目录 修改目录权…

40.x86游戏实战-找出XXX遍历周围的类型

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 工具下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1rEEJnt85npn7N38Ai0_F2Q?pwd6tw3 提…

【C#】中IndexOf的用法

在 C# 中&#xff0c;IndexOf 方法是字符串和列表&#xff08;如 List<T>&#xff09;等数据结构中常用的方法&#xff0c;用于查找指定元素或子串首次出现的位置。以下是针对不同情况使用 IndexOf 的示例。 对于字符串 对于字符串类型&#xff0c;IndexOf 方法返回子字…

scanpy切换显示颜色总结

函数实现 import scanpy as sc adata sc.datasets.pbmc68k_reduced() print(adata) sc.pl.umap(adata,color["bulk_labels"])def change_show_color(adata,label,category_listNone,color_listNone):for i in range(len(color_list)):if(len(color_list[i])7):colo…

【人工智能】Transformers之Pipeline(九):物体检测(object-detection)

目录​​​​​​​ 一、引言 二、物体检测&#xff08;object-detection&#xff09; 2.1 概述 2.2 技术原理 2.3 应用场景 2.4 pipeline参数 2.4.1 pipeline对象实例化参数 2.4.2 pipeline对象使用参数 2.4 pipeline实战 2.5 模型排名 三、总结 一、引言 pipel…

Chromium编译指南2024 - Android篇:前置要求(一)

1.引言 欢迎阅读《Chromium编译指南2024 - Android篇》。本指南旨在帮助开发者理解和掌握在Android平台上编译Chromium的全过程。Chromium是一个开源的浏览器项目&#xff0c;由Google主导开发&#xff0c;并为多个现代浏览器提供基础代码。Android作为全球使用最广泛的移动操…

[Meachines] [Medium] Magic SQLI+文件上传+跳关TRP00F权限提升+环境变量劫持权限提升

信息收集 IP AddressOpening Ports10.10.10.185TCP:22,80 $ nmap -p- 10.10.10.185 --min-rate 1000 -sC -sV PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 2048 06:d4:89:bf:51:…

redis面试(九)锁重入和互斥

可重入 1&#xff09;如果一开始这个锁是没有的&#xff0c;第一次来加锁&#xff0c;这段lua脚本会如何执行&#xff1f; "if (redis.call(‘exists’, KEYS[1]) 0) then " "redis.call(‘hset’, KEYS[1], ARGV[2], 1); " "redis.call(‘pexpi…

[0CTF 2016]piapiapia1

打开题目 看到登录口 字符串绕过长度限制strlen($_POST[nickname]) > 10