java项目| 豆包MarsCode AI刷题

137 阅读16分钟

写在前面

起因是这样的,当我准备了很长时间八股文准备找一段实习工作时,我接到了蔚来的面试,面试官的一个问题让我大脑瞬间就宕机了——你这个项目是怎么实现序列化的?我回想起八股文中序列化是什么,在什么地方需要使用序列化,为什么不推荐使用JDK自带的序列化......可我的项目中到底那里使用了,怎么使用的序列化哪?

于是便有了今天这篇文章,我相信很多人像我一样,跟着某颜色的马敲了一遍外卖项目,但是到头来对这个项目却是一窍不通,现在如果抛开这个,让你自己设计一个项目,你会怎么做哪?你可能知道个大概,比如什么使用MVC架构、redis做缓存、MyBatis做持久化,可具体的细节你又要怎么实现?比如那部分需要做持久化,怎么做;要不要搞个统一的异常处理,怎么搞。那么下面便跟着我来一起梳理一下这整个外卖项目是怎么实现的,都做了哪些操作以及做这些操作的原因吧!

概述

首先我们可以看到整个项目分为了3层的结构,分布是:common,pojo,server,我先来讲解一下这3层的大致构造:

common:存放公共类,也就是项目中server层共同使用的部分,常见的比如String常量来定义的各种字段,下面是一个简单的例子:

  • 复制
  • 删除

**

  • Plain Text
  • C++
  • Java
  • C
  • Python2
  • Python3
  • Pypy2
  • Pypy3
  • JavaScript
  • PHP
  • C#
  • R
  • Go
  • Ruby
  • Rust
  • Bash
  • Shell
  • CSS
  • HTML
  • XML
  • ASP/VB
  • Perl
  • Swift
  • Objective-C
  • Pascal
  • MATLAB
  • Scala
  • Kotlin
  • Groovy
  • Typescript
  • SQL
  • MySQL
  • Oracle
  • SQLite
  • Scheme
  • TCL

复制

删除

xxxxxxxxxx

6

 

1

public class MessageConstant {

2

    public static final String PASSWORD_ERROR = "密码错误";

3

    public static final String ACCOUNT_NOT_FOUND = "账号不存在";

4

    public static final String ACCOUNT_LOCKED = "账号被锁定";

5

    public static final String ALREADY_EXISTS="已存在";

6

}

通过这种方法就可以让我们在项目中直接使用英文字符而不需要打字,虽然这么看着有点脱裤子放屁,但是这种做法确实有一定道理,比如将来需要对提示信息之类进行修改时就不用到项目中去挨个找了,实现了一个解耦。

pojo:存放实体类,也就是普通java对象,MVC架构的model便对应这部分,这里有

  • dto(数据传输对象)数据在代码间互相传递而使用的对象,主要用于服务层和控制器层之间的数据传输,减少网络传输的数据量,提高性能。
  • entity(实体对象)也就是对应这数据库的哪些表和字段的部分
  • vo(视图对象)对应着在前端界面上显示的那些部分,封装了从后端传递到前端的数据。

为什么要分成这三个部分哪,直接使用一种对象怎么样?这也体现了一个解耦的好处,通过设置成3种不同的模块便让安全性和性能都有一定的优化,比如在使用VO来向前端传递数据时就可以把那些不必要的信息或者没有用的信息给过滤掉,传的少了响应速度变高了。至于在什么位置用了哪种对象我将在server层详细指出

serve:最核心的部分,整个后端的服务都在这里,包括什么员工,管理员登录;用AOP做的公共字段处理;JWT认证等等,大家如果是跟着视频来敲的话也多数是这部分

下面我便来详细讲解一下这3个部分都写了哪些内容:

common

先总览一下这部分都分为了哪些内容:

  1. constant:也就是前文所说的String常量,其中包括:公共字段填充相关常量,JWT令牌中使用到的常量,信息提示常量类,密码常量(其实只有一个默认密码123456),还有状态常量表示(启用禁用)
  2. context:这里面只定义了一个类用于线程空间ThreadLocal的管理,可以看到他把set,get方法都进行了封装,这便是一种规范化,如果直接调用不封装的话,谁知道你在里面存放的是什么东西,提一嘴这里面的ThreadLocal实际上存放的是解密后的jwt令牌,用来实现令牌校验优化的
  3. enumeration:使用了一个枚举类,使用枚举的地方还真不多这里应该是唯一处了,这个枚举只定义两个字段update,insert用来记录数据库操作类型
  4. exception:统一的异常处理部分,可以看这里面有非常多的类,但每个类只提供了2个方法,一个是无参构造,另一个有参构造方法(String类型的msg用来传递异常信息),每个类都继承了BaseException,而其又继承自RuntimeException,也就是对运行时异常的封装处理
  5. json:将java对象转为json和将json转为java对象的部分(也就是面试官问我你项目怎么实现序列化的部分,沟槽的原来在这里),此类继承 ObjectMapper,这是 Jackson 库中的一个核心类,用于 JSON 的序列化和反序列化。这里面定义了三个String静态常量,这种写法是对时间显示形式的规范定义方法;在构造方法中分为了3个部分:1. configure 方法, ObjectMapper 类提供的一个方法,用于配置 ObjectMapper 的行为,这里面的第一个参数实际上是父类提供的枚举值并非是我们定义的,第二个参便是告诉他不启用这个属性,既收到未知属性时不报异常;2. 同理设置为反序列化时的属性不存在兼容处理,解释一下这个this.xx是什么操作,这是指当前类的实例和对其使用的方法;3. 剩下的这一大串实际上是注册的一个自定义序列化模块,这个模块是对时间信息的序列化和反序列化,addSerializer 方法(序列化方法java对象转json)有两个参数:type:要序列化类型的class对象,serializer:自定义的序列化器实例,这里使用new来新建的。而addDeserializer 方法便是反序列化,最后将这个自定义的模块进行注册使其生效。通过这个部分便能实现前端的传过来的一个json数据要如何与java中的对象来做对应和如何把一个java对象转为json发给前端。在这里我们只是对时间和日期的格式进行了单独的处理,而其他部分Jackson所提供的功能已经足够满足我们使用了
  6. properties:这是用来做部分配置的,比如阿里的OSS,微信的小程序,还有jwt令牌相关,以jwt为例,这里用到了3个注解 @Component 和 @Data 这两个应该都懂吧,注册bean和生成set,get 那些 ,而@ConfigurationProperties指向了配置文件application.yml的jwt字段,这样的话便让String变量和文件中设置的属性相互对应也就是右边那个(多提一嘴,在go中通常是用viper来做这些的,结果现在才发现spring把这些都集成起来了真是挺强大的)
  7. result:分为了2个类,第一个类是对分页查询结果的封装,分别使用了@Date ,@AllArgsConstructor 和 @NoArgsConstructor 注解,用途为自动生成一个包含所有字段的构造方法和一个无参的构造方法。并且实现了 Serializable 接口,使该类的对象可以被序列化,便于在网络传输或持久化存储中使用。第二个类是对返回结果的统一处理,这里统一定义了Result泛型类,其包含3个字段分别是Integer的code,String的msg和T的data,并提供了有参和无参的两个success方法和error方法,在server层的controller中,我们都是使用其作为返回值来表示是否执行成功并返回数据给前端。

utils:工具类

一共有4个类,其中阿里oss的请求文件上传等和微信小程序的服务部分这里不做展开,这里详细讲解一下jwt认证部分

  • 复制
  • 删除

**

  • Plain Text
  • C++
  • Java
  • C
  • Python2
  • Python3
  • Pypy2
  • Pypy3
  • JavaScript
  • PHP
  • C#
  • R
  • Go
  • Ruby
  • Rust
  • Bash
  • Shell
  • CSS
  • HTML
  • XML
  • ASP/VB
  • Perl
  • Swift
  • Objective-C
  • Pascal
  • MATLAB
  • Scala
  • Kotlin
  • Groovy
  • Typescript
  • SQL
  • MySQL
  • Oracle
  • SQLite
  • Scheme
  • TCL

复制

删除

xxxxxxxxxx

25

 

1

public class JwtUtil {

2

    /**

3

     * 生成jwt

4

     * 使用Hs256算法, 私匙使用固定秘钥             《这部分是为了注释此方法的传入参数是什么等》

5

     * @param secretKey jwt秘钥

6

     * @param ttlMillis jwt过期时间(毫秒)

7

     * @param claims    设置的信息

8

     * @return

9

     */

10

    public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {

11

        // 指定签名的时候使用的签名算法,也就是header那部分

12

        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

13

        // 生成JWT的时间

14

        long expMillis = System.currentTimeMillis() + ttlMillis;

15

        Date exp = new Date(expMillis);

16

        // 设置jwt的body

17

        JwtBuilder builder = Jwts.builder()

18

                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的

19

                .setClaims(claims)

20

                // 设置签名使用的签名算法和签名使用的秘钥

21

                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))

22

                // 设置过期时间

23

                .setExpiration(exp);

24

        return builder.compact();

25

    }

虽然这段代码给出了注释但是我相信大部分人可能还有点困惑,这里解释一下,我们都知道Jwt由3部分组成:Header,Payload,Signature,在这里需要我们关心的是前两个,其中Header的签名算法的设置在11行(哈希256),而Payload中设置了claims(需要传递的信息),和exp(过期的时间),而第21行设置的秘钥并非写入Payload中,秘钥是保存在服务端的这里的方法调用是为了令其生成Signature。最后调用compact()将整个令牌作为String返回

  • 复制
  • 删除

**

  • Plain Text
  • C++
  • Java
  • C
  • Python2
  • Python3
  • Pypy2
  • Pypy3
  • JavaScript
  • PHP
  • C#
  • R
  • Go
  • Ruby
  • Rust
  • Bash
  • Shell
  • CSS
  • HTML
  • XML
  • ASP/VB
  • Perl
  • Swift
  • Objective-C
  • Pascal
  • MATLAB
  • Scala
  • Kotlin
  • Groovy
  • Typescript
  • SQL
  • MySQL
  • Oracle
  • SQLite
  • Scheme
  • TCL

复制

删除

xxxxxxxxxx

9

 

1

public static Claims parseJWT(String secretKey, String token) {

2

        // 得到DefaultJwtParser

3

        Claims claims = Jwts.parser()

4

                // 设置签名的秘钥

5

                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))

6

                // 设置需要解析的jwt

7

                .parseClaimsJws(token).getBody();

8

        return claims;

9

    

令牌校验的部分就比较简单,通过秘钥就可以将token转成我们想要的数据了

pojo

这样可以明显看到dto的部分是最多的,在这个项目中我们使用了DTO用来从前端“接”数据,使用VO向前端“发”数据,而使用entity来封装/存放数据库数据

而在vo和entity中可以看到使用了大量的:@Data、@NoArgsConstructor 和 @AllArgsConstructor。通过这些注解可以帮助我们快速生产大量的构造方法,get/set方法。而@Builder可以让你通过链式方法来快速为对象赋值,比如我们返回结果为success时返回视图对象就使用这种方式创建并赋值

server

这一层是最多最复杂的部分,整体结构如下:

annotation:此处只定义了一个注解 AutoFill 用于标识哪些方法需要进行字段自动填充,如下@Target(ElementType.METHOD):表示该注解只能应用于方法上。@Retention(RetentionPolicy.RUNTIME):表示该注解在运行时保留,可以通过反射获取。而OperationType既上文所说的拥有两个字段的枚举类

aspect:这也就是项目中用到AOP的部分

这里使用的AOP主要作用就是当给数据库更新或插入数据时自动填充更新时间等数据

头部说明:可以看见此类使用了3个注解,其中

@Aspect:声明该类为一个切面类。

@Component:将该类注册为 Spring 容器中的一个 Bean。

@Slf4j:使用 Lombok 自动生成日志对象 log。

而通过@Pointcut注解将切入点定义为mapper包下带有@AutoFill注解(也就是上面自定义的注解)的方法,这样如果调用那些方法时便会自动在调用前执行下面的前置通知

前置通知:

  • 复制
  • 删除

**

  • Plain Text
  • C++
  • Java
  • C
  • Python2
  • Python3
  • Pypy2
  • Pypy3
  • JavaScript
  • PHP
  • C#
  • R
  • Go
  • Ruby
  • Rust
  • Bash
  • Shell
  • CSS
  • HTML
  • XML
  • ASP/VB
  • Perl
  • Swift
  • Objective-C
  • Pascal
  • MATLAB
  • Scala
  • Kotlin
  • Groovy
  • Typescript
  • SQL
  • MySQL
  • Oracle
  • SQLite
  • Scheme
  • TCL

复制

删除

xxxxxxxxxx

17

 

1

@Before("autoFillPointCut()")//前置通知

2

    public void autoFill(JoinPoint joinPoint){

3

        log.info("开始进行公共字段自动填充...");//记录日志

4

        //获取当前被拦截方法上的注解对象

5

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象

6

        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象

7

        //获取注解中定义的操作类型

8

        OperationType operationType = autoFill.value();

9

        Object[] args = joinPoint.getArgs();

10

        if (args.length == 0||args == null){

11

            return;

12

        }

13

        //获取当前被拦截方法参数的值-实体对象

14

        Object entity = args[0];

15

        //准备赋值数据

16

        LocalDateTime now=LocalDateTime.now();

17

        Long currentId = BaseContext.getCurrentId();

MethodSignature 是 Signature 的一个子接口,提供了更多关于方法的信息,如方法名、参数类型等,通过 MethodSignature 对象,我们可以获取到被拦截方法的详细信息。然后通过定义的autoFill实例来获得方法注解的对象,目的就是为了得到其标注的信息是insert还是update

接下来使用joinPoint.getArgs()来获取被拦截方法的第一个参数值,既需要填充字段的实体对象

  • 复制
  • 删除

**

  • Plain Text
  • C++
  • Java
  • C
  • Python2
  • Python3
  • Pypy2
  • Pypy3
  • JavaScript
  • PHP
  • C#
  • R
  • Go
  • Ruby
  • Rust
  • Bash
  • Shell
  • CSS
  • HTML
  • XML
  • ASP/VB
  • Perl
  • Swift
  • Objective-C
  • Pascal
  • MATLAB
  • Scala
  • Kotlin
  • Groovy
  • Typescript
  • SQL
  • MySQL
  • Oracle
  • SQLite
  • Scheme
  • TCL

复制

删除

xxxxxxxxxx

18

 

1

        //根据当前不同的操作类型,为对应的属性通过反射赋值

2

        if(operationType == OperationType.INSERT){

3

            try {

4

                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

5

                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);

6

                Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);

7

                Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);

8

9

                //通过反射为对象的属性赋值

10

                setCreateTime.invoke(entity,now);

11

                setCreateUser.invoke(entity,currentId);

12

                setUpdateTime.invoke(entity,now);

13

                setUpdateUser.invoke(entity,currentId);

14

15

            }catch (Exception e){

16

                e.printStackTrace();

17

            }

18

        }else if(operationType == OperationType.UPDATE) {

如果识别到注解上的为insert操作,则尝试用反射获取实体类中的set方法对象,并调用invoke方法(反射的方式,如果了解过反射的话一定对这段函数不陌生)稍微讲解一下,entity.getClass().getDeclaredMethod() 就是获得之前得到的需要填充字段的实体对象的Class对象,然后对class对象使用getDeclaredMethod()获得其中的方法(包括私有和公用的),两个参数分别为该方法的名称(这里的方法名写了一大堆,不用怀疑就是在common中定义的String常量)和其参数类型的class对象

有的人可能就好奇了这个setXX方法在哪里?我怎么没有找到,其实就在entity中比如Dish,这里就有创建/删除时间的定义,而他又使用了@Date注解,虽然看不到实际的方法但其实都自动生成好了

注解为更新的同理。

config:redis的配置和Mvc的配置

reids的配置部分,使用@Configuration:表示这是一个配置类(也是后文SpringCache生效的关键所在),Spring 容器会扫描这个类并加载其中的 Bean。接着对方法使用Bean注解交给Spring容器管理,之后的使用便可以直接注入

Mvc的配置部分:其中省略了通过knife4j(swagger)来生成接口文档的部分

  • 复制
  • 删除

**

  • Plain Text
  • C++
  • Java
  • C
  • Python2
  • Python3
  • Pypy2
  • Pypy3
  • JavaScript
  • PHP
  • C#
  • R
  • Go
  • Ruby
  • Rust
  • Bash
  • Shell
  • CSS
  • HTML
  • XML
  • ASP/VB
  • Perl
  • Swift
  • Objective-C
  • Pascal
  • MATLAB
  • Scala
  • Kotlin
  • Groovy
  • Typescript
  • SQL
  • MySQL
  • Oracle
  • SQLite
  • Scheme
  • TCL

复制

删除

xxxxxxxxxx

41

 

1

@Configuration

2

@Slf4j

3

public class WebMvcConfiguration extends WebMvcConfigurationSupport {

4

    @Autowired

5

    private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;

6

    @Autowired

7

    private JwtTokenUserInterceptor jwtTokenUserInterceptor;

8

    /**

9

     * 注册自定义拦截器

10

     * @param registry

11

     */

12

    protected void addInterceptors(InterceptorRegistry registry) {

13

        log.info("开始注册自定义拦截器...");

14

        registry.addInterceptor(jwtTokenAdminInterceptor)

15

                .addPathPatterns("/admin/**")

16

                .excludePathPatterns("/admin/employee/login");

17

  

18

        registry.addInterceptor(jwtTokenUserInterceptor)

19

                .addPathPatterns("/user/**")

20

                .excludePathPatterns("/user/user/login")

21

                .excludePathPatterns("/user/shop/status");

22

    }

23

    /**

24

     * 设置静态资源映射

25

     * @param registry

26

     */

27

    protected void addResourceHandlers(ResourceHandlerRegistry registry) {

28

        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");

29

        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");

30

    }

31

    //扩展MVC消息转换器

32

    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {

33

        log.info("扩展消息转换器...");

34

        //创建消息转换器对象

35

        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();

36

        //需要设置对象转换器,底层使用Jackson将Java对象转为json

37

        converter.setObjectMapper(new JacksonObjectMapper());

38

        //将消息转换器对象追加到MVC框架的转换器集合中

39

        converters.add(0,converter);

40

    }

41

}

这个仍旧使用了@Configuration注解,这样Spring 会自动扫描并加载该类中的配置,并且继承了 WebMvcConfigurationSupport类(对 Spring MVC 配置的扩展支持),包含两个自动注入的私有变量,这些类将在后面说明,用途为用户和员工的jwt令牌校验拦截器

  1. 第一个方法就是设置使用了何种拦截器对什么请求路径进行性拦截,这种链式方法调用可以视为一种固定用法。
  2. 第二个方法把比如url尾部为 /doc.html 的请求路径映射到 classpath:/META-INF/resources/ 目录下的 doc.html 文件,在这里的classpath 指JVM 在加载类和资源文件时搜索的路径。在 Maven中,classpath 通常包括:src/main/java:存放 Java 源代码。src/main/resources:存放资源文件,如配置文件、静态资源等。而这个项目中静态资源已经被放置了nginx中,项目中也没有META-INF文件包
  3. 第三个方法为消息转化器,这里便使用到了前文在common中所设定的JacksonObjectMapper(),json和java对象的转化器,这种设定方法就类似:1.声明一个东西,2.将自己的修改插入设定的东西中,3.让这个东西生效(可以看到在前面那么远声明的类在这个地方才得到使用,而且仅有这一个地方被使用过)