当前位置: 首页 > news >正文

SpringBoot框架(邮件发送Mail|持久层框架JPA|Extra前后端分离跨域处理|接口管理Swagger)这一篇就够了(超详细)

🙈作者简介:练习时长两年半的Java up主
🙉个人主页:老茶icon
🙊 ps:点赞👍是免费的,却可以让写博客的作者开兴好久好久😎
📚系列专栏:Java全栈,计算机系列(火速更新中)
💭 格言:种一棵树最好的时间是十年前,其次是现在
🏡动动小手,点个关注不迷路,感谢宝子们一键三连

目录

  • 课程名:SpringBoot
    • 内容/作用:知识点/设计/实验/作业/练习
    • 学习:SpringBoot框架(邮件发送Mail|持久层框架JPA|Extra前后端分离跨域处理|接口管理Swagger)
  • SpringBoot其他框架
    • 邮件发送:Mail
      • 邮件发送
      • 邮件注册
    • 持久层框架:JPA
      • 认识SpringDataJPA
      • 使用JPA
      • 方法名称拼接自定义SQL
      • 关联查询
      • JPQL自定义SQL语句
    • Extra. 前后端分离跨域处理
    • 接口管理:Swagger
      • 走进Swagger
      • 接口信息配置
  • 总结

课程名:SpringBoot

内容/作用:知识点/设计/实验/作业/练习

学习:SpringBoot框架(邮件发送Mail|持久层框架JPA|Extra前后端分离跨域处理|接口管理Swagger)

SpringBoot其他框架

通过了解其他的SpringBoot框架,我们就可以在我们自己的Web服务器上实现更多更高级的功能。

邮件发送:Mail

我们在注册很多的网站时,都会遇到邮件或是手机号验证,也就是通过你的邮箱或是手机短信去接受网站发给你的注册验证信息,填写验证码之后,就可以完成注册了,同时,网站也会绑定你的手机号或是邮箱。

那么,像这样的功能,我们如何实现呢?SpringBoot已经给我们提供了封装好的邮件模块使用:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId>
</dependency>

邮件发送

在学习邮件发送之前,我们需要先了解一下什么是电子邮件。

电子邮件也是一种通信方式,是互联网应用最广的服务。通过网络的电子邮件系统,用户可以以非常低廉的价格(不管发送到哪里,都只需负担网费,实际上就是把信息发送到对方服务器而已)、非常快速的方式,与世界上任何一个地方的电子邮箱用户联系。

虽说方便倒是方便,虽然是曾经的霸主,不过现在这个时代,QQ微信横行,手机短信和电子邮箱貌似就只剩收验证码这一个功能了。

要在Internet上提供电子邮件功能,必须有专门的电子邮件服务器。例如现在Internet很多提供邮件服务的厂商:新浪、搜狐、163、QQ邮箱等,他们都有自己的邮件服务器。这些服务器类似于现实生活中的邮局,它主要负责接收用户投递过来的邮件,并把邮件投递到邮件接收者的电子邮箱中。

所有的用户都可以在电子邮件服务器上申请一个账号用于邮件发送和接收,那么邮件是以什么样的格式发送的呢?实际上和Http一样,邮件发送也有自己的协议,也就是约定邮件数据长啥样以及如何通信。

在这里插入图片描述

比较常用的协议有两种:

  1. SMTP协议(主要用于发送邮件 Simple Mail Transfer Protocol)
  2. POP3协议(主要用于接收邮件 Post Office Protocol 3)

整个发送/接收流程大致如下:

在这里插入图片描述

实际上每个邮箱服务器都有一个smtp发送服务器和pop3接收服务器,比如要从QQ邮箱发送邮件到163邮箱,那么我们只需要通过QQ邮箱客户端告知QQ邮箱的smtp服务器我们需要发送邮件,以及邮件的相关信息,然后QQ邮箱的smtp服务器就会帮助我们发送到163邮箱的pop3服务器上,163邮箱会通过163邮箱客户端告知对应用户收到一封新邮件。

而我们如果想要实现给别人发送邮件,那么就需要连接到对应电子邮箱的smtp服务器上,并告知其我们要发送邮件。而SpringBoot已经帮助我们将最基本的底层通信全部实现了,我们只需要关心smtp服务器的地址以及我们要发送的邮件长啥样即可。

这里以163邮箱 https://mail.163.com 为例,我们需要在配置文件中告诉SpringBootMail我们的smtp服务器的地址以及你的邮箱账号和密码,首先我们要去设置中开启smtp/pop3服务才可以,开启后会得到一个随机生成的密钥,这个就是我们的密码。

spring:mail:# 163邮箱的地址为smtp.163.com,直接填写即可host: smtp.163.com# 你申请的163邮箱username: javastudy111@163.com# 注意密码是在开启smtp/pop3时自动生成的,记得保存一下,不然就找不到了password: AZJTOAWZESLMHTNI

配置完成后,接着我们来进行一下测试:

@SpringBootTest
class SpringBootTestApplicationTests {//JavaMailSender是专门用于发送邮件的对象,自动配置类已经提供了Bean@AutowiredJavaMailSender sender;@Testvoid contextLoads() {//SimpleMailMessage是一个比较简易的邮件封装,支持设置一些比较简单内容SimpleMailMessage message = new SimpleMailMessage();//设置邮件标题message.setSubject("【电子科技大学教务处】关于近期学校对您的处分决定");//设置邮件内容message.setText("XXX同学您好,经监控和教务巡查发现,您近期存在旷课、迟到、早退、上课刷抖音行为," +"现已通知相关辅导员,请手写5000字书面检讨,并在2022年4月1日17点前交到辅导员办公室。");//设置邮件发送给谁,可以多个,这里就发给你的QQ邮箱message.setTo("你的QQ号@qq.com");//邮件发送者,这里要与配置文件中的保持一致message.setFrom("javastudy111@163.com");//OK,万事俱备只欠发送sender.send(message);}}

如果需要添加附件等更多功能,可以使用MimeMessageHelper来帮助我们完成:

@Test
void contextLoads() throws MessagingException {//创建一个MimeMessageMimeMessage message = sender.createMimeMessage();//使用MimeMessageHelper来帮我们修改MimeMessage中的信息MimeMessageHelper helper = new MimeMessageHelper(message, true);helper.setSubject("Test");helper.setText("lbwnb");helper.setTo("你的QQ号@qq.com");helper.setFrom("javastudy111@163.com");//发送修改好的MimeMessagesender.send(message);
}

邮件注册

既然我们已经了解了邮件发送,那么我们接着来看如何在我们的项目中实现邮件验证。

首先明确验证流程:请求验证码 -> 生成验证码(临时有效,注意设定过期时间) -> 用户输入验证码并填写注册信息 -> 验证通过注册成功!


在这里插入图片描述

持久层框架:JPA

  • 用了Mybatis之后,你看那个JDBC,真是太逊了。
  • 这么说,你的项目很勇哦?
  • 开玩笑,我的写代码超勇的好不好。
  • 阿伟,你可曾幻想过有一天你的项目里不再有SQL语句?
  • 不再有SQL语句?那我怎么和数据库交互啊?
  • 我看你是完全不懂哦
  • 懂,懂什么啊?
  • 你想懂?来,到我项目里来,我给你看点好康的。
  • 好康?是什么新框架哦?
  • 什么新框架,比新框架还刺激,还可以让你的项目登duang郎哦。
  • 哇,杰哥,你项目里面都没SQL语句诶,这是用的什么框架啊?

在我们之前编写的项目中,我们不难发现,实际上大部分的数据库交互操作,到最后都只会做一个事情,那就是把数据库中的数据映射为Java中的对象。比如我们要通过用户名去查找对应的用户,或是通过ID查找对应的学生信息,在使用Mybatis时,我们只需要编写正确的SQL语句就可以直接将获取的数据映射为对应的Java对象,通过调用Mapper中的方法就能直接获得实体类,这样就方便我们在Java中数据库表中的相关信息了。

但是以上这些操作都有一个共性,那就是它们都是通过某种条件去进行查询,而最后的查询结果,都是一个实体类,所以你会发现你写的很多SQL语句都是一个套路select * from xxx where xxx=xxx,那么能否有一种框架,帮我们把这些相同的套路给封装起来,直接把这类相似的SQL语句给屏蔽掉,不再由我们编写,而是让框架自己去组合拼接。

认识SpringDataJPA

首先我们来看一个国外的统计:

不对吧,为什么Mybatis这么好用,这么强大,却只有10%的人喜欢呢?然而事实就是,在国外JPA几乎占据了主导地位,而Mybatis并不像国内那样受待见,所以你会发现,JPA都有SpringBoot的官方直接提供的starter,而Mybatis没有。

至于为啥SSM阶段不讲这个,而是放到现在来讲也是因为,在微服务场景下它的优势才能更多的发挥出来。

那么,什么是JPA?

JPA(Java Persistence API)和JDBC类似,也是官方定义的一组接口,但是它相比传统的JDBC,它是为了实现ORM而生的,即Object-Relationl Mapping,它的作用是在关系型数据库和对象之间形成一个映射,这样,我们在具体的操作数据库的时候,就不需要再去和复杂的SQL语句打交道,只要像平时操作对象一样操作它就可以了。

在之前,我们使用JDBC或是Mybatis来操作数据,通过直接编写对应的SQL语句来实现数据访问,但是我们发现实际上我们在Java中大部分操作数据库的情况都是读取数据并封装为一个实体类,因此,为什么不直接将实体类直接对应到一个数据库表呢?也就是说,一张表里面有什么属性,那么我们的对象就有什么属性,所有属性跟数据库里面的字段一一对应,而读取数据时,只需要读取一行的数据并封装为我们定义好的实体类既可以,而具体的SQL语句执行,完全可以交给框架根据我们定义的映射关系去生成,不再由我们去编写,因为这些SQL实际上都是千篇一律的。

而实现JPA规范的框架一般最常用的就是Hibernate,它是一个重量级框架,学习难度相比Mybatis也更高一些,而SpringDataJPA也是采用Hibernate框架作为底层实现,并对其加以封装。

官网:https://spring.io/projects/spring-data-jpa

使用JPA

同样的,我们只需要导入stater依赖即可:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

接着我们可以直接创建一个类,比如账户类,我们只需要把一个账号对应的属性全部定义好即可:

@Data
public class Account {int id;String username;String password;
}

接着,我们可以通过注解形式,在属性上添加数据库映射关系,这样就能够让JPA知道我们的实体类对应的数据库表长啥样。

@Data
@Entity   //表示这个类是一个实体类
@Table(name = "users")    //对应的数据库中表名称
public class Account {@GeneratedValue(strategy = GenerationType.IDENTITY)   //生成策略,这里配置为自增@Column(name = "id")    //对应表中id这一列@Id     //此属性为主键int id;@Column(name = "username")   //对应表中username这一列String username;@Column(name = "password")   //对应表中password这一列String password;
}

接着我们来修改一下配置文件:

spring:jpa:#开启SQL语句执行日志信息show-sql: truehibernate:#配置为自动创建ddl-auto: create

ddl-auto属性用于设置自动表定义,可以实现自动在数据库中为我们创建一个表,表的结构会根据我们定义的实体类决定,它有4种

  • create 启动时删数据库中的表,然后创建,退出时不删除数据表
  • create-drop 启动时删数据库中的表,然后创建,退出时删除数据表 如果表不存在报错
  • update 如果启动时表格式不一致则更新表,原有数据保留
  • validate 项目启动表结构进行校验 如果不一致则报错

我们可以在日志中发现,在启动时执行了如下SQL语句:

Hibernate: create table users (id integer not null auto_increment, password varchar(255), username varchar(255), primary key (id)) engine=InnoDB

而我们的数据库中对应的表已经创建好了。

我们接着来看如何访问我们的表,我们需要创建一个Repository实现类:

@Repository
public interface AccountRepository extends JpaRepository<Account, Integer> {}

注意JpaRepository有两个泛型,前者是具体操作的对象实体,也就是对应的表,后者是ID的类型,接口中已经定义了比较常用的数据库操作。编写接口继承即可,我们可以直接注入此接口获得实现类:

@SpringBootTest
class JpaTestApplicationTests {@ResourceAccountRepository repository;@Testvoid contextLoads() {//直接根据ID查找repository.findById(1).ifPresent(System.out::println);}}

运行后,成功得到查询结果。我们接着来测试增删操作:

@Test
void addAccount(){Account account = new Account();account.setUsername("Admin");account.setPassword("123456");account = repository.save(account);  //返回的结果会包含自动生成的主键值System.out.println("插入时,自动生成的主键ID为:"+account.getId());
}
@Test
void deleteAccount(){repository.deleteById(2);   //根据ID删除对应记录
}
@Test
void pageAccount() {repository.findAll(PageRequest.of(0, 1)).forEach(System.out::println);  //直接分页
}

我们发现,使用了JPA之后,整个项目的代码中没有出现任何的SQL语句,可以说是非常方便了,JPA依靠我们提供的注解信息自动完成了所有信息的映射和关联。

相比Mybatis,JPA几乎就是一个全自动的ORM框架,而Mybatis则顶多算是半自动ORM框架。

方法名称拼接自定义SQL

虽然接口预置的方法使用起来非常方便,但是如果我们需要进行条件查询等操作或是一些判断,就需要自定义一些方法来实现,同样的,我们不需要编写SQL语句,而是通过方法名称的拼接来实现条件判断,这里列出了所有支持的条件判断名称:

DistinctfindDistinctByLastnameAndFirstnameselect distinct … where x.lastname = ?1 and x.firstname = ?2
AndfindByLastnameAndFirstname… where x.lastname = ?1 and x.firstname = ?2
OrfindByLastnameOrFirstname… where x.lastname = ?1 or x.firstname = ?2
IsEqualsfindByFirstname,findByFirstnameIs,findByFirstnameEquals… where x.firstname = ?1
BetweenfindByStartDateBetween… where x.startDate between ?1 and ?2
LessThanfindByAgeLessThan… where x.age < ?1
LessThanEqualfindByAgeLessThanEqual… where x.age <= ?1
GreaterThanfindByAgeGreaterThan… where x.age > ?1
GreaterThanEqualfindByAgeGreaterThanEqual… where x.age >= ?1
AfterfindByStartDateAfter… where x.startDate > ?1
BeforefindByStartDateBefore… where x.startDate < ?1
IsNullNullfindByAge(Is)Null… where x.age is null
IsNotNullNotNullfindByAge(Is)NotNull… where x.age not null
LikefindByFirstnameLike… where x.firstname like ?1
NotLikefindByFirstnameNotLike… where x.firstname not like ?1
StartingWithfindByFirstnameStartingWith… where x.firstname like ?1(参数与附加%绑定)
EndingWithfindByFirstnameEndingWith… where x.firstname like ?1(参数与前缀%绑定)
ContainingfindByFirstnameContaining… where x.firstname like ?1(参数绑定以%包装)
OrderByfindByAgeOrderByLastnameDesc… where x.age = ?1 order by x.lastname desc
NotfindByLastnameNot… where x.lastname <> ?1
InfindByAgeIn(Collection<Age> ages)… where x.age in ?1
NotInfindByAgeNotIn(Collection<Age> ages)… where x.age not in ?1
TruefindByActiveTrue()… where x.active = true
FalsefindByActiveFalse()… where x.active = false
IgnoreCasefindByFirstnameIgnoreCase… where UPPER(x.firstname) = UPPER(?1)

比如我们想要实现根据用户名模糊匹配查找用户:

@Repository
public interface AccountRepository extends JpaRepository<Account, Integer> {//按照表中的规则进行名称拼接,不用刻意去记,IDEA会有提示List<Account> findAllByUsernameLike(String str);
}
@Test
void test() {repository.findAllByUsernameLike("%T%").forEach(System.out::println);
}

又比如我们想同时根据用户名和ID一起查询:

@Repository
public interface AccountRepository extends JpaRepository<Account, Integer> {Account findByIdAndUsername(int id, String username);//可以使用Optional类进行包装,Optional<Account> findByIdAndUsername(int id, String username);List<Account> findAllByUsernameLike(String str);
}
@Test
void test() {System.out.println(repository.findByIdAndUsername(1, "Test"));
}

比如我们想判断数据库中是否存在某个ID的用户:

@Repository
public interface AccountRepository extends JpaRepository<Account, Integer> {Account findByIdAndUsername(int id, String username);List<Account> findAllByUsernameLike(String str);boolean existsAccountById(int id);
}
@Test
void test() {System.out.println(repository.existsAccountByUsername("Test"));
}

注意自定义条件操作的方法名称一定要遵循规则,不然会出现异常:

Caused by: org.springframework.data.repository.query.QueryCreationException: Could not create query for public abstract  ...

关联查询

在实际开发中,比较常见的场景还有关联查询,也就是我们会在表中添加一个外键字段,而此外键字段又指向了另一个表中的数据,当我们查询数据时,可能会需要将关联数据也一并获取,比如我们想要查询某个用户的详细信息,一般用户简略信息会单独存放一个表,而用户详细信息会单独存放在另一个表中。当然,除了用户详细信息之外,可能在某些电商平台还会有用户的购买记录、用户的购物车,交流社区中的用户帖子、用户评论等,这些都是需要根据用户信息进行关联查询的内容。

img

我们知道,在JPA中,每张表实际上就是一个实体类的映射,而表之间的关联关系,也可以看作对象之间的依赖关系,比如用户表中包含了用户详细信息的ID字段作为外键,那么实际上就是用户表实体中包括了用户详细信息实体对象:

@Data
@Entity
@Table(name = "users_detail")
public class AccountDetail {@Column(name = "id")@GeneratedValue(strategy = GenerationType.IDENTITY)@Idint id;@Column(name = "address")String address;@Column(name = "email")String email;@Column(name = "phone")String phone;@Column(name = "real_name")String realName;
}

而用户信息和用户详细信息之间形成了一对一的关系,那么这时我们就可以直接在类中指定这种关系:

@Data
@Entity
@Table(name = "users")
public class Account {@GeneratedValue(strategy = GenerationType.IDENTITY)@Column(name = "id")@Idint id;@Column(name = "username")String username;@Column(name = "password")String password;@JoinColumn(name = "detail_id")   //指定存储外键的字段名称@OneToOne    //声明为一对一关系AccountDetail detail;
}

在修改实体类信息后,我们发现在启动时也进行了更新,日志如下:

Hibernate: alter table users add column detail_id integer
Hibernate: create table users_detail (id integer not null auto_increment, address varchar(255), email varchar(255), phone varchar(255), real_name varchar(255), primary key (id)) engine=InnoDB
Hibernate: alter table users add constraint FK7gb021edkxf3mdv5bs75ni6jd foreign key (detail_id) references users_detail (id)

是不是感觉非常方便!都懒得去手动改表结构了。

接着我们往用户详细信息中添加一些数据,一会我们可以直接进行查询:

@Test
void pageAccount() {repository.findById(1).ifPresent(System.out::println);
}

查询后,可以发现,得到如下结果:

Hibernate: select account0_.id as id1_0_0_, account0_.detail_id as detail_i4_0_0_, account0_.password as password2_0_0_, account0_.username as username3_0_0_, accountdet1_.id as id1_1_1_, accountdet1_.address as address2_1_1_, accountdet1_.email as email3_1_1_, accountdet1_.phone as phone4_1_1_, accountdet1_.real_name as real_nam5_1_1_ from users account0_ left outer join users_detail accountdet1_ on account0_.detail_id=accountdet1_.id where account0_.id=?
Account(id=1, username=Test, password=123456, detail=AccountDetail(id=1, address=四川省成都市青羊区, email=8371289@qq.com, phone=1234567890, realName=本伟))

也就是,在建立关系之后,我们查询Account对象时,会自动将关联数据的结果也一并进行查询。

那要是我们只想要Account的数据,不想要用户详细信息数据怎么办呢?我希望在我要用的时候再获取详细信息,这样可以节省一些网络开销,我们可以设置懒加载,这样只有在需要时才会向数据库获取:

@JoinColumn(name = "detail_id")
@OneToOne(fetch = FetchType.LAZY)    //将获取类型改为LAZY
AccountDetail detail;

接着我们测试一下:

@Transactional   //懒加载属性需要在事务环境下获取,因为repository方法调用完后Session会立即关闭
@Test
void pageAccount() {repository.findById(1).ifPresent(account -> {System.out.println(account.getUsername());   //获取用户名System.out.println(account.getDetail());  //获取详细信息(懒加载)});
}

接着我们来看看控制台输出了什么:

Hibernate: select account0_.id as id1_0_0_, account0_.detail_id as detail_i4_0_0_, account0_.password as password2_0_0_, account0_.username as username3_0_0_ from users account0_ where account0_.id=?
Test
Hibernate: select accountdet0_.id as id1_1_0_, accountdet0_.address as address2_1_0_, accountdet0_.email as email3_1_0_, accountdet0_.phone as phone4_1_0_, accountdet0_.real_name as real_nam5_1_0_ from users_detail accountdet0_ where accountdet0_.id=?
AccountDetail(id=1, address=四川省成都市青羊区, email=8371289@qq.com, phone=1234567890, realName=卢本)

可以看到,获取用户名之前,并没有去查询用户的详细信息,而是当我们获取详细信息时才进行查询并返回AccountDetail对象。

那么我们是否也可以在添加数据时,利用实体类之间的关联信息,一次性添加两张表的数据呢?可以,但是我们需要稍微修改一下级联关联操作设定:

@JoinColumn(name = "detail_id")
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) //设置关联操作为ALL
AccountDetail detail;
  • ALL:所有操作都进行关联操作
  • PERSIST:插入操作时才进行关联操作
  • REMOVE:删除操作时才进行关联操作
  • MERGE:修改操作时才进行关联操作

可以多个并存,接着我们来进行一下测试:

@Test
void addAccount(){Account account = new Account();account.setUsername("Nike");account.setPassword("123456");AccountDetail detail = new AccountDetail();detail.setAddress("重庆市渝中区解放碑");detail.setPhone("1234567890");detail.setEmail("73281937@qq.com");detail.setRealName("张三");account.setDetail(detail);account = repository.save(account);System.out.println("插入时,自动生成的主键ID为:"+account.getId()+",外键ID为:"+account.getDetail().getId());
}

可以看到日志结果:

Hibernate: insert into users_detail (address, email, phone, real_name) values (?, ?, ?, ?)
Hibernate: insert into users (detail_id, password, username) values (?, ?, ?)
插入时,自动生成的主键ID为:6,外键ID为:3

结束后会发现数据库中两张表都同时存在数据。

接着我们来看一对多关联,比如每个用户的成绩信息:

@JoinColumn(name = "uid")  //注意这里的name指的是Score表中的uid字段对应的就是当前的主键,会将uid外键设置为当前的主键
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)   //在移除Account时,一并移除所有的成绩信息,依然使用懒加载
List<Score> scoreList;
@Data
@Entity
@Table(name = "users_score")   //成绩表,注意只存成绩,不存学科信息,学科信息id做外键
public class Score {@GeneratedValue(strategy = GenerationType.IDENTITY)@Column(name = "id")@Idint id;@OneToOne   //一对一对应到学科上@JoinColumn(name = "cid")Subject subject;@Column(name = "socre")double score;@Column(name = "uid")int uid;
}
@Data
@Entity
@Table(name = "subjects")   //学科信息表
public class Subject {@GeneratedValue(strategy = GenerationType.IDENTITY)@Column(name = "cid")@Idint cid;@Column(name = "name")String name;@Column(name = "teacher")String teacher;@Column(name = "time")int time;
}

在数据库中填写相应数据,接着我们就可以查询用户的成绩信息了:

@Transactional
@Test
void test() {repository.findById(1).ifPresent(account -> {account.getScoreList().forEach(System.out::println);});
}

成功得到用户所有的成绩信息,包括得分和学科信息。

同样的,我们还可以将对应成绩中的教师信息单独分出一张表存储,并建立多对一的关系,因为多门课程可能由同一个老师教授(千万别搞晕了,一定要理清楚关联关系,同时也是考验你的基础扎不扎实):

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "tid")   //存储教师ID的字段,和一对一是一样的,也会当前表中创个外键
Teacher teacher;

接着就是教师实体类了:

@Data
@Entity
@Table(name = "teachers")
public class Teacher {@Column(name = "id")@GeneratedValue(strategy = GenerationType.IDENTITY)@Idint id;@Column(name = "name")String name;@Column(name = "sex")String sex;
}

最后我们再进行一下测试:

@Transactional
@Test
void test() {repository.findById(3).ifPresent(account -> {account.getScoreList().forEach(score -> {System.out.println("课程名称:"+score.getSubject().getName());System.out.println("得分:"+score.getScore());System.out.println("任课教师:"+score.getSubject().getTeacher().getName());});});
}

成功得到多对一的教师信息。

最后我们再来看最复杂的情况,现在我们一门课程可以由多个老师教授,而一个老师也可以教授多个课程,那么这种情况就是很明显的多对多场景,现在又该如何定义呢?我们可以像之前一样,插入一张中间表表示教授关系,这个表中专门存储哪个老师教哪个科目:

@ManyToMany(fetch = FetchType.LAZY)   //多对多场景
@JoinTable(name = "teach_relation",     //多对多中间关联表joinColumns = @JoinColumn(name = "cid"),    //当前实体主键在关联表中的字段名称inverseJoinColumns = @JoinColumn(name = "tid")   //教师实体主键在关联表中的字段名称
)
List<Teacher> teacher;

接着,JPA会自动创建一张中间表,并自动设置外键,我们就可以将多对多关联信息编写在其中了。

JPQL自定义SQL语句

虽然SpringDataJPA能够简化大部分数据获取场景,但是难免会有一些特殊的场景,需要使用复杂查询才能够去完成,这时你又会发现,如果要实现,只能用回Mybatis了,因为我们需要自己手动编写SQL语句,过度依赖SpringDataJPA会使得SQL语句不可控。

使用JPA,我们也可以像Mybatis那样,直接编写SQL语句,不过它是JPQL语言,与原生SQL语句很类似,但是它是面向对象的,当然我们也可以编写原生SQL语句。

比如我们要更新用户表中指定ID用户的密码:

@Repository
public interface AccountRepository extends JpaRepository<Account, Integer> {@Transactional    //DML操作需要事务环境,可以不在这里声明,但是调用时一定要处于事务环境下@Modifying     //表示这是一个DML操作@Query("update Account set password = ?2 where id = ?1") //这里操作的是一个实体类对应的表,参数使用?代表,后面接第n个参数int updatePasswordById(int id, String newPassword);
}
@Test
void updateAccount(){repository.updatePasswordById(1, "654321");
}

现在我想使用原生SQL来实现根据用户名称修改密码:

@Transactional
@Modifying
@Query(value = "update users set password = :pwd where username = :name", nativeQuery = true) //使用原生SQL,和Mybatis一样,这里使用 :名称 表示参数,当然也可以继续用上面那种方式。
int updatePasswordByUsername(@Param("name") String username,   //我们可以使用@Param指定名称@Param("pwd") String newPassword);
@Test
void updateAccount(){repository.updatePasswordByUsername("Admin", "654321");
}

通过编写原生SQL,在一定程度上弥补了SQL不可控的问题。

虽然JPA能够为我们带来非常便捷的开发体验,但是正式因为太便捷了,保姆级的体验有时也会适得其反,可能项目开发到后期特别庞大时,就只能从底层SQL语句开始进行优化,而由于JPA尽可能地在屏蔽我们对SQL语句的编写,所以后期优化是个大问题,并且Hibernate相对于Mybatis来说,更加重量级。不过,在微服务的时代,单体项目一般不会太大,而JPA的劣势并没有太明显地体现出来。

有关Mybatis和JPA的对比,可以参考:https://blog.csdn.net/u010253246/article/details/105731204


Extra. 前后端分离跨域处理

我们的项目已经处于前后端分离状态了,那么前后端分离状态和我们之前的状态有什么区别的呢?

  • 不分离:前端页面看到的都是由后端控制,由后端渲染页面或重定向,后端需要控制前端的展示,前端与后端的耦合度很高。比如我们之前都是使用后端来执行重定向操作或是使用Thymeleaf来填充数据,而最终返回的是整个渲染好的页。

img

  • 分离:后端仅返回前端所需的数据,不再渲染HTML页面,不再控制前端的效果。至于前端用户看到什么效果,从后端请求的数据如何加载到前端中,都由前端通过JS等进行动态数据填充和渲染。这样后端只返回JSON数据,前端处理JSON数据并展示,这样前后端的职责就非常明确了。

img

实现前后端分离有两种方案,一种是直接放入SpringBoot的资源文件夹下,但是这样实际上还是在依靠SpringBoot内嵌的Tomcat服务器进行页面和静态资源的发送,我们现在就是这种方案。

另一种方案就是直接将所有的页面和静态资源单独放到代理服务器上(如Nginx),这样我们后端服务器就不必再处理静态资源和页面了,专心返回数据即可,而前端页面就需要访问另一个服务器来获取,虽然逻辑和明确,但是这样会出现跨域问题,实际上就是我们之前所说的跨站请求伪造,为了防止这种不安全的行为发生,所以对异步请求会进行一定的限制。

这里,我们将前端页面和后端页面直接分离进行测试,在登陆时得到如下错误:

Access to XMLHttpRequest at 'http://localhost:8080/api/auth/login' from origin 'http://localhost:63342' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

可以很清楚地看到,在Ajax发送异步请求时,我们的请求被阻止,原因是在响应头中没有包含Access-Control-Allow-Origin,也就表示,如果服务端允许跨域请求,那么会在响应头中添加一个Access-Control-Allow-Origin字段,如果不允许跨域,就像现在这样。那么,什么才算是跨域呢:

  1. 请求协议如http、https不同
  2. 请求的地址/域名不同
  3. 端口不同

因为我们现在相当于前端页面访问的是静态资源服务器,而后端数据是由我们的SpringBoot项目提供,它们是两个不同的服务器,所以在垮服务器请求资源时,会被判断为存在安全风险。

但是现在,由于我们前后端是分离状态,我们希望的是能够实现跨域请求,这时我们就需要添加一个过滤器来处理跨域问题:

@Bean
public CorsFilter corsFilter() {//创建CorsConfiguration对象后添加配置CorsConfiguration config = new CorsConfiguration();//设置放行哪些原始域,这里直接设置为所有config.addAllowedOriginPattern("*");//你可以单独设置放行哪些原始域 config.addAllowedOrigin("http://localhost:2222");//放行哪些原始请求头部信息config.addAllowedHeader("*");//放行哪些请求方式,*代表所有config.addAllowedMethod("*");//是否允许发送Cookie,必须要开启,因为我们的JSESSIONID需要在Cookie中携带config.setAllowCredentials(true);//映射路径UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();corsConfigurationSource.registerCorsConfiguration("/**", config);//返回CorsFilterreturn new CorsFilter(corsConfigurationSource);
}

这样,我们的SpringBoot项目就支持跨域访问了,接着我们再来尝试进行登陆,可以发现已经能够正常访问了,并且响应头中包含了以下信息:

Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Access-Control-Allow-Origin: http://localhost:63342
Access-Control-Expose-Headers: *
Access-Control-Allow-Credentials: true

可以看到我们当前访问的原始域已经被放行了。

但是还有一个问题,我们的Ajax请求中没有携带Cookie信息(这个按理说属于前端知识了)这里我们稍微改一下,不然我们的请求无法确认身份:

function get(url, success){$.ajax({type: "get",url: url,async: true,dataType: 'json',xhrFields: {withCredentials: true},success: success});
}function post(url, data, success){$.ajax({type: "post",url: url,async: true,data: data,dataType: 'json',xhrFields: {withCredentials: true},success: success});
}

添加两个封装好的方法,并且将withCredentials开启,这样在发送异步请求时,就会携带Cookie信息了。

在学习完成Linux之后,我们会讲解如何在Linux服务器上部署Nginx反向代理服务器。


接口管理:Swagger

在前后端分离项目中,前端人员需要知道我们后端会提供什么数据,根据后端提供的数据来进行前端页面渲染(在之前我们也演示过)这个时候,我们就需要编写一个API文档,以便前端人员随时查阅。

但是这样的一个文档,我们也不可能单独写一个项目去进行维护,并且随着我们的后端项目不断更新,文档也需要跟随更新,这显然是很麻烦的一件事情,那么有没有一种比较好的解决方案呢?

当然有,那就是丝袜哥:Swagger

走进Swagger

Swagger的主要功能如下:

  • 支持 API 自动生成同步的在线文档:使用 Swagger 后可以直接通过代码生成文档,不再需要自己手动编写接口文档了,对程序员来说非常方便,可以节约写文档的时间去学习新技术。
  • 提供 Web 页面在线测试 API:光有文档还不够,Swagger 生成的文档还支持在线测试。参数和格式都定好了,直接在界面上输入参数对应的值即可在线测试接口。

结合Spring框架(Spring-fox),Swagger可以很轻松地利用注解以及扫描机制,来快速生成在线文档,以实现当我们项目启动之后,前端开发人员就可以打开Swagger提供的前端页面,查看和测试接口。依赖如下:

<dependency><groupId>io.springfox</groupId><artifactId>springfox-boot-starter</artifactId><version>3.0.0</version>
</dependency>

SpringBoot 2.6以上版本修改了路径匹配规则,但是Swagger3还不支持,这里换回之前的,不然启动直接报错:

spring:mvc:pathmatch:matching-strategy: ant_path_matcher

项目启动后,我们可以直接打开:http://localhost:8080/swagger-ui/index.html,这个页面(要是觉得丑,UI是可以换的,支持第三方)会显示所有的API文档,包括接口的路径、支持的方法、接口的描述等,并且我们可以直接对API接口进行测试,非常方便。

我们可以创建一个配置类去配置页面的相关信息:

@Configuration
public class SwaggerConfiguration {@Beanpublic Docket docket() {return new Docket(DocumentationType.OAS_30).apiInfo(new ApiInfoBuilder().contact(new Contact("你的名字", "https://www.bilibili.com", "javastudy111*@163.com")).title("图书管理系统 - 在线API接口文档").build());}
}

接口信息配置

虽然Swagger的UI界面已经可以很好地展示后端提供的接口信息了,但是非常的混乱,我们来看看如何配置接口的一些描述信息。

首先我们的页面中完全不需要显示ErrorController相关的API,所以我们配置一下选择哪些Controller才会生成API信息:

@Bean
public Docket docket() {ApiInfo info = new ApiInfoBuilder().contact(new Contact("你的名字", "https://www.bilibili.com", "javastudy111@163.com")).title("图书管理系统 - 在线API接口文档").description("这是一个图书管理系统的后端API文档,欢迎前端人员查阅!").build();return new Docket(DocumentationType.OAS_30).apiInfo(info).select()       //对项目中的所有API接口进行选择.apis(RequestHandlerSelectors.basePackage("com.example.controller")).build();
}

接着我们来看看如何为一个Controller编写API描述信息:

@Api(tags = "账户验证接口", description = "包括用户登录、注册、验证码请求等操作。")
@RestController
@RequestMapping("/api/auth")
public class AuthApiController {

我们可以直接在类名称上面添加@Api注解,并填写相关信息,来为当前的Controller设置描述信息。

接着我们可以为所有的请求映射配置描述信息:

@ApiResponses({@ApiResponse(code = 200, message = "邮件发送成功"),  @ApiResponse(code = 500, message = "邮件发送失败")   //不同返回状态码描述
})
@ApiOperation("请求邮件验证码")   //接口描述
@GetMapping("/verify-code")
public RestBean<Void> verifyCode(@ApiParam("邮箱地址")   //请求参数的描述@RequestParam("email") String email){
@ApiIgnore     //忽略此请求映射
@PostMapping("/login-success")
public RestBean<Void> loginSuccess(){return new RestBean<>(200, "登陆成功");
}

我们也可以为实体类配置相关的描述信息:

@Data
@ApiModel(description = "响应实体封装类")
@AllArgsConstructor
public class RestBean<T> {@ApiModelProperty("状态码")int code;@ApiModelProperty("状态码描述")String reason;@ApiModelProperty("数据实体")T data;public RestBean(int code, String reason) {this.code = code;this.reason = reason;}
}

这样,我们就可以在文档中查看实体类简介以及各个属性的介绍了。

最后我们再配置一下多环境:

<profiles><profile><id>dev</id><activation><activeByDefault>true</activeByDefault></activation><properties><environment>dev</environment></properties></profile><profile><id>prod</id><activation><activeByDefault>false</activeByDefault></activation><properties><environment>prod</environment></properties></profile>
</profiles>
<resources><resource><directory>src/main/resources</directory><excludes><exclude>application*.yaml</exclude></excludes></resource><resource><directory>src/main/resources</directory><filtering>true</filtering><includes><include>application.yaml</include><include>application-${environment}.yaml</include></includes></resource>
</resources>

首先在Maven中添加两个环境,接着我们配置一下不同环境的配置文件:

  jpa:show-sql: falsehibernate:ddl-auto: update
springfox:documentation:enabled: false

在生产环境下,我们选择不开启Swagger文档以及JPA的数据库操作日志,这样我们就可以根据情况选择两套环境了。

总结

   好好学习,天天向上。

往期专栏
Java全栈开发
数据结构与算法
计算机组成原理
操作系统
数据库系统
物联网控制原理与技术

相关文章:

SpringBoot框架(邮件发送Mail|持久层框架JPA|Extra前后端分离跨域处理|接口管理Swagger)这一篇就够了(超详细)

&#x1f648;作者简介&#xff1a;练习时长两年半的Java up主 &#x1f649;个人主页&#xff1a;老茶icon &#x1f64a; ps:点赞&#x1f44d;是免费的&#xff0c;却可以让写博客的作者开兴好久好久&#x1f60e; &#x1f4da;系列专栏&#xff1a;Java全栈&#xff0c;计…...

chatGPT对话R语言

文章目录 R语言介绍R语言基本语法R语言常用函数有哪些R语言数据结构向量矩阵数组和列表数组列表 数据框因子 R如何导入数据如何在R语言中导出数据&#xff1f;R语言图形绘制描述性统计描述统计也可以这样来计算 统计推断配对设计t检验样本均数和总体均数t检验两&#xff08;独立…...

代码随想录--字符串--替换空格题型

①这道题可以直接申请一个临时数组&#xff0c;然后遍历字符串&#xff0c;是空格则加入20%&#xff0c;最后再把临时数组转化为字符串。 怎么把一个数组转化为字符串? 如数组arry[]&#xff0c; string newstr new string(arry,0,arry.size()-1); return newstr; 而且临时数…...

Spring JDBC和事务控制

目录 Spring JDBC 和 事务控制主要内容Spring 整合 JDBC 环境构建项目添加依赖坐标添加 jdbc 配置文件编写 spring 配置文件配置数据源C3P0 数据源配置DBCP 数据源配置 模板类配置Spring JDBC 测试 &#xff08;入门&#xff09;创建指定数据库创建数据表使用 JUnit 测试JUnit …...

【音视频第16天】详解STUN协议

一个webRTC传输协议搞得自己云里雾里的。现在主动攻克一下。先看看STUN协议。好&#xff0c;我们开始吧 目录 1.讲讲什么是NAT&#xff1f;2.NAT有啥问题&#xff1f;3.四种NAT类型4.STUN Server5.TURN ServerSTUN和TURN的实现&#xff1a;什么是STUN&#xff1f;为什么需要ST…...

C++算法初级11——01背包问题(动态规划2)

C算法初级11——01背包问题&#xff08;动态规划2&#xff09; 文章目录 C算法初级11——01背包问题&#xff08;动态规划2&#xff09;问题引入0-1背包问题分析0-1背包问题的形式化分析优化 问题引入 辰辰采药 辰辰是个天资聪颖的孩子&#xff0c;他的梦想是成为世界上最伟大…...

Python 库大全(下)

格式化输出 模块 reprlib 提供了一份定制的 repr()&#xff0c;用于简洁 地展示各种大的或者多层嵌套的容器变量&#xff1a; >>> import reprlib >>> reprlib.repr(set(\supercalifragilisticexpialidocious\)) "{\a\, \c\, \d\, \e\, \f\, \g\, ...…...

如何用链表实现LRU缓存淘汰算法

链表学习 一、 缓存1.1缓存介绍1.2 缓存策略 二、链表结构2.1 单链表2.2 循环链表2.3 双向链表2.4 双向循环链表2.5 链表与数组性能对比 三、如何基于链表实现LRU缓存淘汰算法 一、 缓存 1.1缓存介绍 缓存是一种提高数据读取性能的技术&#xff0c;在硬件设计、软件开发中都有…...

【01】数据结构与算法基础-数据、数据元素、数据项和数据对象 | 数据类型和抽象数据类型 | 抽象数据类型的表示和C++实现

目录) 0.数据结构的研究内容1.数据、数据元素、数据项和数据对象1.1数据1.2数据元素(Data element)和数据项1.3数据项1.4数据对象1.5数据结构(Data Structure)1.6逻辑结构的种类1.7存储结构的种类2.数据类型和抽象数据类型2.1数据类型(Data Type)2.2抽象数据类型(Abstract …...

PHP匿名类的使用场景有哪些?PHP匿名类怎么用?有什么好处?PHP匿名类如何在运行时动态生成?

以下是一些使用匿名类的场景&#xff1a; 2. 简单的工厂模式&#xff1a;当需要在运行时动态创建一些简单的对象时&#xff0c;可以使用匿名类替代创建不必要的类定义和文件。 function createGreeter($name) {return new class($name) {private $name;public function __cons…...

【并发基础】一篇文章带你彻底搞懂Java线程中断的底层原理——interrupt()、interrupted()、isInterrupted()

目录 〇、Java线程中断与阻塞的区别 0.1 线程中断 0.2 线程阻塞 一、线程的中断 二、中断方法 2.1 void interrupt() 2.1.1 可中断的阻塞 2.1.2 不可中断的阻塞 2.1.3 实践案例 2.2 boolean isInterrupted() 2.3 boolean interrupted() 2.4 代码案例 三、源码分析…...

【c语言】函数的数据传递原理 | 数组传入函数方法

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; 给大家跳段街舞感谢支持&#xff01;ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ…...

vue3源码(3)——computed

Vue3中的computed底层源码主要是通过使用Proxy对象来实现的。下面是对Vue3中computed底层源码的详细解读&#xff1a; 在Vue3中&#xff0c;computed的实现是通过使用createComputed函数来创建的。createComputed函数接收两个参数&#xff1a;getter和setter。 在createComput…...

数学建模第七天:数学建模算法篇之插值及MATLAB实现

目录 一、前言 1、引例 2、拟合定义 3、拟合与插值的关系 二、拟合 1、线性最小二乘法求解 ①思路 ②解法 2、MATLAB对线性最小二乘拟合的实现 ①函数说明 ②求解例题 3、MATLAB实现非线性曲线拟合 ①lsqcurvefit函数 ②代码求解 4、MATLAB实现非线性最小二乘拟…...

RUST 每日一省:生命周期作用域

生命周期 一个变量的生命周期就是它从创建到销毁的整个过程。 作用域 我们声明的每个变量都有作用域。作用域其实是变量和值存在的环境。作用域是由一对花括号表示的。例如&#xff0c;使用块表达式会创建一个作用域&#xff0c;即任何以花括号开头和结尾的表达式。此…...

【过程8】——能量守恒视角总结感受

一、背景 另一个角度的看到&#xff0c;观望着过程中自己曾经类似的经历(小舅子的工作)。 时间久了&#xff0c;经历多了&#xff0c;感悟会更加的充实&#xff1b;最近自己对于人在维持能量的过程中也有很多的感悟&#xff0c;一并做一下总结 二、过程 1.人为什么天性不愿意…...

kong(4):限流配置

Kong 提供了 Rate Limiting 插件&#xff0c;实现对请求的限流功能&#xff0c;避免过大的请求量过大&#xff0c;将后端服务打挂。 Rate Limiting 支持秒/分/小时/日/月/年多种时间维度的限流&#xff0c;并且可以组合使用。例如说&#xff1a;限制每秒最 多 100 次请求&…...

人脸识别 Face Recognition 入门

人脸识别 Face Recognition 入门概述 总述传统特征方法深度学习方法损失函数改进基于欧几里德和距离的损失基于角度/余弦边距的损失SoftMax 损失及其变体 一级标题二级标题二级标题二级标题 找论文搭配 Sci-Hub 食用更佳 &#x1f4aa; Sci-Hub 实时更新 : https://tool.yovisu…...

【AI绘画】Midjourney的使用及程序示例

Midjourney 1.背景2.Midjourney的原理3.Midjourney的使用方法4.Midjourney的示例代码 1.背景 Midjourney 是一款基于深度学习的图像转换工具&#xff0c;其可以将一张图像转换成具有不同风格的图像&#xff0c;例如将一张照片转换成卡通风格的图像。Midjourney 基于 TensorFlow…...

无公网IP?教你在外远程访问本地wamp服务器「内网穿透」

目录 前言 1.Wamp服务器搭建 1.1 Wamp下载和安装 1.2 Wamp网页测试 2. Cpolar内网穿透的安装和注册 2.1 本地网页发布 2.2 Cpolar云端设置 2.3 Cpolar本地设置 3. 公网访问测试 4. 结语 前言 软件技术的发展日新月异&#xff0c;各种能方便我们生活、工作和娱乐的新…...

leetcode 628. 三个数的最大乘积

题目描述解题思路执行结果 leetcode 628. 三个数的最大乘积 题目描述 三个数的最大乘积 给你一个整型数组 nums &#xff0c;在数组中找出由三个数组成的最大乘积&#xff0c;并输出这个乘积。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,3] 输出&#xff1a;6 示例 2&…...

fork函数如何创建进程,exit/_exit函数如何使进程终止的详细分析与代码实现

&#x1f38a;【进程通信与并发】专题正在持续更新中&#xff0c;进程&#xff0c;线程&#xff0c;IPC&#xff0c;线程池等的创建原理与运用✨&#xff0c;欢迎大家前往订阅本专题&#xff0c;获取更多详细信息哦&#x1f38f;&#x1f38f;&#x1f38f; &#x1fa94;本系列…...

重置电脑时提示“缺少所需的驱动器分区”怎么办?

当您启动Windows 10电脑并收到“您的电脑/设备需修复”这个消息提示时&#xff0c;您会马上尝试修复电脑&#xff0c;如果您这样做了&#xff0c;您可能会收到一个“安装Windows的驱动器已被锁定”的信息。如果您尝试重置您的电脑&#xff0c;您可能会收到一条提示&#xff0c;…...

在KylinV10安装Dm8

前言 因为近期&#xff0c;业外和几个朋友想搞点有趣的项目玩玩&#xff0c;既然不以盈利为主&#xff0c;就> 主推国产化&#xff0c;所以这篇记录一下&#xff0c;我在KylinV10安装dm8.最近真的很忙&#xff0c;要负责专研一下国产化工具开发的事&#xff0c;还要负责tb级…...

「SQL面试题库」 No_46 交换工资

&#x1f345; 1、专栏介绍 「SQL面试题库」是由 不是西红柿 发起&#xff0c;全员免费参与的SQL学习活动。我每天发布1道SQL面试真题&#xff0c;从简单到困难&#xff0c;涵盖所有SQL知识点&#xff0c;我敢保证只要做完这100道题&#xff0c;不仅能轻松搞定面试&#xff0…...

SLAM论文速递【SLAM—— RDS-SLAM:基于语义分割方法的实时动态SLAM—4.24(1)

论文信息 题目&#xff1a; RDS-SLAM:Real-Time Dynamic SLAM Using Semantic Segmentation Methods RDS-SLAM:基于语义分割方法的实时动态SLAM论文地址&#xff1a; https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber9318990发表期刊&#xff1a; IEEE Access ( Volum…...

OJ练习第82题——填充书架

填充书架 力扣链接&#xff1a;1105. 填充书架 题目描述 给定一个数组 books &#xff0c;其中 books[i] [thicknessi, heighti] 表示第 i 本书的厚度和高度。你也会得到一个整数 shelfWidth 。 按顺序 将这些书摆放到总宽度为 shelfWidth 的书架上。 先选几本书放在书架…...

OHOS IDE和SDK的安装方法

参照OpenHarmony应用开发环境安装流程&#xff0c;下载安装OHOS的IDE&#xff0c;过程中需要全程联网。 IDE&#xff0c;安装至D:\Tools\Huawei\DevEcoStudio。 IDE安装成功之后&#xff0c;按照提示下载安装HOS和OHOS的SDK。 nodejs&#xff0c;安装至D:\Tools\Huawei\nodejs…...

New Year Garland(计数类DP)

New Year Garland 题意 ​ 用m种颜色的球装饰n层的圣诞树&#xff0c;圣诞树的第i层由 l i l_{i} li​个彩球串成&#xff0c;且同一层相邻的球颜色不同&#xff0c;相邻的层之间彩球颜色的集合不同&#xff0c;问有多少种方案&#xff0c;对p取模。 分析 ​ 首先先计算每一…...

32岁阿里P7,把简历改成不知名小公司,学历改成普通本科,工作内容不变,投简历全挂!...

hr靠什么来招人&#xff1f; 一位猎头讲述了自己和朋友打赌的故事&#xff1a; 朋友在阿里云&#xff0c;32岁&#xff0c;P7&#xff0c;他把简历上的公司改成不知名&#xff0c;学历改成普通本科&#xff0c;工作内容不变&#xff0c;结果投其他公司&#xff08;比如京东&…...