本文最后更新于 2023-11-23,文章内容可能已经过时。

Springboot笔记

Springboot的复杂性不是来自他处理的对象,而是来自他自身,是来自不断演进发展的springboot带来的时间维度上的复杂性.

1.Web前后端分离基本原理:Spring MVC

用户在view(视图层)进行操作,该层调用controller的接口进行逻辑处理,controller调用service层的方法,service层的数据持久化又需要dao层的数据库支持.

2.Springboot介绍:

Springboot是一个java ee框架,是Spring的升级版,采用前后端分离的方式产出接口,实现网页,微信小程序,app的开发,他的特点是简化配置,部署.监控

3.Springboot项目上手:

3.1 创建Springboot项目:

idea:file-new-Spring Initializr

3.2 Springboot项目结构:

新建项目后,会得到一个文件夹,其中一级文件需要关注最重要的是src文件夹和pom.xml文件,pom.xml是依赖(依赖?可以理解认为一些外部包)的配置文件,其中src包括main文件夹和text文件夹,text文件夹存放测试文件,main包括java文件夹(用于写业务程序),resources文件夹(存放项目的配置)

3.3 新建Controller层:

3.3.1 Controller介绍:

在java文件夹下,创建类名为某Controller类进行编写。Controller是接口层,是直接和前端交互的地方,它由一个一个的接口组成.(接口:接收一部分数据,进行内部处理,返回另一组数据)

3.3.2 注解:

在类上方增加@RestController注解

1
2
@RestController
public class  ctivityController {}

用于标识这个类是Controller类,同时这个注解是@Controller和@ResposeBody两个注解的结合,可以直接接收前端的json格式的数据,同时返回json格式的数据

3.3.3 基本结构:

Controller类由一个一个的接口构成,其基本格式如下:

1
2
3
4
@PostMapping("/addScore")
public Result addScore (@RequestParam("addStrips") List<AddStrip> addStrips){
    return activityService.addScore(name, addStrips);
}

接口就是一些注解加上一个方法,必须要有的是形如@PostMapping一样的注解,表明了这个接口访问的地址,以及访问的方式等信息

而对于需要传参数的接口,需要加类似@RequestParam类的接口,加在方法所需的参数前,表明了这个参数传来的名称与格式。

3.3.4: Post与Get请求:

post请求的参数在reposeBody中,可以使用@RequestBody获取数据.get请求的参数体现在url中,不能使用@RequestBody获取数据。

一般因为post请求实现了信息的隐藏,在url中看不到,所以认为post请求比get请求更安全。

3.3.5:@RequestParam与@RequestBody:

3.3.5.1: @RequestParam:

用来处理Content-Type: 为 application/x-www-form-urlencoded编码的内容。(Http协议中,如果不指定Content-Type,则默认传递的参数就是application/x-www-form-urlencoded类型.POST类型和GET类型都可以使用@RequestParam注解来接收参数.

@RequestParam注解有三个参数,required 表示是否必须,默认为 true,必须。defaultValue 可设置请求参数的默认值,如果设置了该值,required=true将失效,自动为false,如果没有传该参数,就使用默认值。value 为接收url的参数名(相当于key值).

3.3.5.2: application/x-www-form-urlencoded:

application/x-www-form-urlencoded:是最常见的 POST 提交数据的方式,浏览器的原生表单如果不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded 方式提交数据,它是未指定属性时的默认值。 数据发送过程中会对数据进行序列化处理,以键值对形式?key1=value1&key2=value2的方式发送到服务器。 数据被编码成以 ‘&’ 分隔的键-值对, 同时以 ‘=’ 分隔键和值。非字母或数字的字符会被 percent-encoding。在axios中当请求参数为qs.stringify(data)时,会以此方式提交数据。后台如果使用对象接收的话,可以自动封装成对象.

3.3.5.3: @RequestBody:

@RequestBody接收的参数是来自requestBody中,即请求体。一般用于处理非 Content-Type: application/x-www-form-urlencoded编码格式的数据,比如:application/json、application/xml等类型的数据。

3.3.5.4: application/json:

随着 json 规范的越来越流行,并且对浏览器支持程度原来越好,许多开发人员在请求头中加入 content-type: application/jsonapplication/json ,这样做可以方便的提交复杂的结构化数据,这样特别适合restful接口。它告诉服务器请求的主体内容是 json 格式的字符串,服务器端会对json字符串进行解析,json 格式要支持比键值对复杂得多的结构化数据。这种方式的好处就是前端人员不需要关心数据结构的复杂度,只需要标准的json格式就能提交成功。当在 axios 中请求参数为普通对象时,POST 请求默认发送的是 application/json 格式的数据。 application/json 需要封装成对象的话,可以加上 @RequestBody 进行注解。

综上,当传输实体类属性时,用@RequestParam(用@RequestBody)也可,但是传入List类型的数据时,由于@RequestParam所要求的application/x-www-form-urlencoded识别不了,所以只能通过@RequestBody传输数据.

@RequestBody与@RequestParam()可以同时使用,@RequestBody最多只能有一个,而@RequestParam()可以有多个。

4. 主启动类:

在java文件夹下,自动生成的一个类,下面有一个主方法。

这个类用于项目的启动,不需要做任何的修改。

5.修改项目配置:

位于resources文件夹下,为了更快的写配置,删除掉该文件夹下原有的application.properties文件,新建一个application.yml文件

格式:

properties:

key=value

yml:

key: value(注意value前面有一个空格)

完成前三步后,就可以运行程序,并在网页上访问相应的接口的到相应的数据,但这样不能实现数据的变化,而要做到这一点,就需要进行数据库操作.

6.数据库操作:

6.1 技术选型:

选用mysql数据库,使用jpa进行操作

jpa:jpa: java persistence(持久化) api ,一种对象持久化的标准.

spring-data-jpa:根据jpa开发出的一个产品.

6.2 操作过程:

6.2.1 添加maven依赖:

spring-data-jpa.

mysql-connector-java

6.2.2 yml配置:

1
2
3
4
5
6
7
8
9
10
11
datasource:
  driver-class-name: com.mysql.cj.jdbc.Driver
  url: jdbc:mysql://localhost:3306/dbaddscore(你自己的数据库名)
  					ip地址:端口
  username: root
  password: (你自己的密码)
jpa:
  hibernate:
    ddl-auto: update(意味着没有数据就新建,有数据也保留),还可用create(每次运行都覆盖数据)等
  show-sql: true(在控制台展示sql语句)
  

6.2.3 链接数据库:

用Navicat与数据库连接,在里面创建即可,编码选择UTF-8.

只需要新建数据库即可,无需自己建表.

6.2.4: 编写实体类:

6.2.4.1 实体类注解:

完成数据库连接后即可编写实体类,实体类是会数据库中储存的类.

建立类后,在类的上方加@Entity注解,表示这是一个实体类,在项目运行时,会自动创建由这个类所映射的表,以及里面的所有数据.

注意:在此类中一定要有一个无参的构造方法,否则会报错,可以在类上方通过

1
2
@NoArgsConstructor
@AllArgsConstructor

这两个注解来实现.

6.2.4.2 数据库主键:

数据库主键是这一行数据在这张表中唯一的标识,在一张表中,每条数据的这个字段一定不相同,可以作为其他表的外键,让其他表通过这个属性的值找到这条记录

主键注解:

1
2
3
4
5
6
/**
 * id
 */
@Id  用于表征接下来这个数据是主键
@GeneratedValue(strategy = GenerationType.IDENTITY) 主键策略,当前是主键自动生成
private Integer id;

6.2.5: 建立数据库接口:

建立接口名为实体类名+Repository,继承JpaRepository,同时在类上方添加@Repository注解

1
2
@Repository
public interface ActivityRepository extends JpaRepository<Activity,Integer> {}

第一个参数是类名,第二个参数是类的主键数据类型.

在没有额外方法的时候,数据库中不用写任何代码.

6.2.6: 接口实例化:

1
2
@Autowired
UserRepository userRepository;

在所需要的类的上方添加这个,就可以使用这个新建的对象调用数据库中各种方法.

简单的数据库操作都用自带的方法:如

.findAll() 返回数据库中所有数据.

.save() 保存一个对象

6.2.7: 修改对象数据:

修改时,通过find方法找到对象,修改其中的数据,注意不要修改其主键,然后重新使用数据库保存,数据库就会根据主键自动覆盖之前的数据.

6.2.8 自定义数据库操作:

jpa中共有三种方法:

(1) 默认的方法:

(2) 自定义规则名的方法:

只需要按规定的jpa格式写好对应的方法名,就能自动获得方法,不用自己写,如:

List findActivityByYear(Integer year);

(3) 自定义sql语句:

使用@Query注解(注意jdbc和jpa两个包都支持这个注解,注意不要导错包):

格式为:

1
2
@Query(value = "sql语句",nativeQuery = "")
方法相关信息

nativeQuery默认为false,意为hql,即字段用的是实体类中的名字,true表示sql,字段用的是数据库中的名字.

6.2.9 级联操作:

6.2.9.1: 介绍:

级联操作用于维护有关系的几张表之间的关系.本质上是用一张表的主键数据,成为另一张上的外键数据,操作级联更新,级联删除等操作.

启用级联操作时,当更新一个表的主键值,系统将会相应的更新其对应的所有外键值,如果删除主键,相应的外键也会一同删除.

比如角色有多个课程,课程也会被多个角色选.适合于多对多关系.jpa会自动生成中间表,java的entity代码中只需要User类和Role类,无需创建中间表user_role类(sql建表语句中可以手动创建该中间表,不手动创建jpa系统也会帮忙自动创建).

当角色类维护外键时,若角色名字改变,课程列表中的角色名也会随之改变,若角色删除,课程列表中也会删除这个学生,

但课程没有维护外键的能力,也就是说当课程名称改变时,学生课表中课程的姓名不会改变,课程删除时,学生课表中的课程也不会删除.

6.2.9.2: 使用:

以manytomany的多对多关系为例子,进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在控制类的关联字段上加上如下配置:
@ManyToMany(fetch = FetchType.LAZY,targetEntity = User.class)
//fetch:可取的值有FetchType.EAGER和FetchType.LAZY,前者表示主类被加载时加载,后者表示被访问时才会加载
@JoinTable(name="users_courses",uniqueConstraints = {@UniqueConstraint(columnNames = {"u_id","c_id"})},
joinColumns = {@JoinColumn(name = "u_id",referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "c_id",referencedColumnName = "id"),}
//name:主键名称
//uniqueConstraints:唯一约束的联合主键
//joinColumns:当前对象在中间表中的外键,此处应为id
//inverseJoinColumns:对方对象在中间表中的外键,此处为id
//一个角色有多个课程
private List<Course> courses = new ArrayList<>();

在另一方的关联字段下面加上如下配置:
//一个课程可以被多个角色选择
//使得course放弃外键的维护权利
@ManyToMany(mappedBy = "courses",cascade = CascadeType.ALL)
//这个cascade字段定义了级联的类型.主要有:CascadeType.PERSIST(级联新建)、CascadeType.REMOVE(级联删除)、CascadeType.REFRESH(级联刷新)、CascadeType.MERGE(级联更新)、CascadeType.ALL(选择全部).一般不推荐使用all(?).
private List<User> users = new ArrayList<>();

7.单元测试:

7.1 service层测试:

对应于不进行数据库和接口访问,只针对某种特定的逻辑(比如测试获取当前时间是否正确等)

在text文件夹下创建测试类,在类前加@SpringBootTest注解.

在这个类下面写方法,每个方法前面加@Test注解,每个方法都可以单独,直接运行进行测试.

注意方法不能有返回值,股返回值的类型是void.

8.其他技术:

前七部分已经可以写出一般的业务逻辑,其他的零零碎碎的技术,用于更好地优化逻辑和提升代码质量.

8.1:结果类:

1
2
3
4
5
6
7
8
9
public class Result<T>{
    //    返回的状态码
    private int code;
    //    返回的状态信息
    private String message;
    //    返回的具体数据
    private T data;

}

后端向前端返回数据时,不直接返回数据,而是一般封装一个结果类进行返回,便于前端获取返回结果的状态(返回成功or出现错误),同时获取返回的具体信息,最后获取返回的数据.由于不知道返回的具体类型,故使用泛型.

传入什么类型均可,在无返回具体数据时可返回null.

给接口传入参数时,框架自动把传入的json转换成java对象,结果返回时,同样由框架把java对象转换成json对象.

8.2: 项目配置:

application的dev和prod配置分开.

1
2
3
4
5
6
7
8
9
10
11
spring:
  freemarker:
    check-template-location: false
  profiles:
    active: dev
  jackson:
    time-zone: GMT+8
  data:
    redis:
      repositories:
        enabled: false

使用主配置文件里profiles字段来选择使用的配置文件.

8.3 项目部署:

8.3.1: 打包:

右上角maven图标,依次点击刷新,clean,package,自动在文件目录下生成一个jar包.

8.3.2: 连接服务器

8.3.3: 部署:

(1)进入文件夹 cd 文件夹名

(2)上传jar包

(3)运行jar包

nohup java -jar jar包名称.jar > 日志名 2>&1

(4)根据端口查看进程:

lsof -i:端口名

如果不输出即意味着这个端口当前没有进程

(5)杀死进程:

kill -9 进程名

8.4: 跨域问题:

跨域:从一个域名访问另外一个域名时,域名.端口.协议任何一个不一样都会造成无法访问的情况.

预检:进行跨域访问时,首先会发送预检请求,如果服务器允许的话才会再次发送真正的(options)请求.

解决方案:cors:cross-origin resource sharing 一种跨域资源共享方案.

8.4.1 简单请求:

没有人为添加字段的get,post,head(?)的请求,解决方式:

在Controller类上方添加@CrossOrigin(origins = “*”)注解

8.4.2 复杂请求:

当需要在header中添加token等字段时,需要使用全局的跨域配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
//表征这是一个配置,只有这个配置才会被框架识别并自动使用.
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter(){

        UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.setAllowCredentials(false);
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**",corsConfiguration);
        CorsFilter corsFilter = new CorsFilter(urlBasedCorsConfigurationSource);
        return corsFilter;
    }
}

8.5: 事务处理:

事务:对数据库的若干操作构成的集合.

事物的出现是为了操作的可靠性,通过回退机制使数据安全.

事务分为编程式事务(通过手写代码)和声明式事务(通过AOP)两种方式.

声明式事务:在方法或者类(再类上添加表示对类下的所有方法生效)上加@Tranactional注解

当该方法出现异常时,该方法中对数据库的所有的操作不会被提交.

比如在进行取款操作时,如果没有事务,若在从银行取出钱后,发生了异常导致用户没有收到钱,就会导致钱的总数减少从而出现问题,而对这个方法使用事务就可以很好地解决这个问题.

8.6: AOP(面向切面编程):

8.6.1 定义:

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP(面向对象编程)的延续,是软件开发中的一个
热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高
了开发的效率。

简单来说,就是在面向对象的,纵向设计的复杂程序中,横向的关注程序的不同位置中复用代码的地方(切面),进行统一的处理.比如打印程序执行日志之类的.

8.6.2 概念:

切面(aspect): 类似于java类,在里面包含着一些pointcut以及对应的advice.

连接点(joint point): 程序中明确定义的点,比如说方法调用,异常处理什么的.一般所有的同类过程都可以被认为是连接点.

切点(point cut): 切面的组成部分,表示一组特定的连接点,advice发生在这些地方.

增强(advice): 表明了对在pointcut里面定义的过程所进行的操作.

8.6.3 基本使用:

以打印执行日志为例子.

8.6.3.1 引入依赖:
1
2
3
4
5
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    <version>2.7.0</version>
</dependency>
8.6.3.2 新建一个切面类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.util;


import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;



//新建切面
@Aspect
//这个注解的作用是表明这是一个切面类
@Component
//这个注解的作用是把当前类注入到Spring容器内
public class LogAspect {
    
    //切点:这个注解用来筛选所有的连接点,可以使用通配符.
    @Pointcut("execution(public * com.controller.*.*(..))")
    public void LogAspect(){}
    
    
    //这是对切点的增强(advice)
    //这些方法的参数是JoinPoint,里面包含了类名,被切面的方法名,被切面的方法参数等信息.
    
    @Before("LogAspect()")
    //在方法前执行这个方法
    public void doBefore(JoinPoint joinPoint){
        System.out.println("doBefore");
    }

    @After("LogAspect()")
    //在方法后执行这个方法
    public void doAfter(JoinPoint joinPoint){
        System.out.println("doAfter");
    }

    @AfterReturning("LogAspect()")
    //在方法执行后,并且返回结果后执行
    public void doAfterReturning(JoinPoint joinPoint){
        System.out.println("doAfterReturning");
    }

    @AfterThrowing("LogAspect()")
    //在方法执行后,并且抛出异常后执行
    public void deAfterThrowing(JoinPoint joinPoint){
        System.out.println("deAfterThrowing");
    }

    @Around("LogAspect()")
    //环绕:在方法执行前后都执行.参数必须为ProceedingJoinPoint
    public Object deAround(ProceedingJoinPoint joinPoint) throws Throwable{
        System.out.println("deAround");
       	//获取当前类
        Class<?> clazz = joinPoint.getClass();
        //获取请求参数
        Object[] args = joinPoint.getArgs();
        System.out.println(("当前类名是:"+clazz.getName()));
        System.out.println("传入参数是"+ Arrays.toString(args));
        return joinPoint.proceed();
    }
    
}

还有其他很多种使用方式,后续再一一进行学习.

8.7: 异常处理:

异常处理机制。异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰。

8.7.1 Java异常体系结构:

Java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。Throwable又派生出Error类和Exception类。

错误:Error类以及它的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。

异常:Exception以及它的子类,代表程序运行时发生的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。

Error和Exception的区别:

Error和Exception都有一个共同的根类是Throwable类。

Error是系统中的错误,程序员是不能改变的和处理的,一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。因此我们编写程序时不需要关心这类错误。

Exception,也就是我们经常见到的一些异常情况,表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。

img

异常通常分为两类:受检异常和运行异常.

受检异常(checked exception)是编译器要求必须处理的异常,否则编译不会通过.一般是一些发生概率非常高的异常.

运行异常则可处理也可不处理.相对来说一般发生的概率较低.

注意:虽然叫受检异常,但是编译阶段不会发生异常,因为异常就是new 异常对象,而只有运行时才可new对象,所以所有的异常都是程序运行时产生的.

img

8.7.2 Java异常处理机制1: try-catch-finally结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
语法格式:
    try{
        ...... //可能产生异常的代码
    }
    catch( ExceptionName1 e ){
        ...... //当产生ExceptionName1型异常时的处置措施
    }
    catch( ExceptionName2 e ){
        ...... //当产生ExceptionName2型异常时的处置措施
    }
    [ finally{
        ...... //无论是否发生异常,都无条件执行的语句
    } ]
 
语法解释:
    try:
        捕获异常的第一步是用try{…}语句块选定捕获异常的范围,将可能出现异常的代码放在try语句块中。
        如果发生异常,则尝试去匹配catch块,catch块可以有多个(因为try块可以出现多个不同类型异常);
        如果执行完try不管有没有发生异常,则接着去执行finally块和finally后面的代码(如果有的话)。			
    catch (Exceptiontype e):
        在catch语句块中是对异常对象进行处理的代码。每个try语句块可以伴随一个或多个catch语句,用于处理可能产生的不同类型的异常对象。
        每一个catch块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java可以将多个异常声明在一个catch中。 catch(Exception1 | Exception2 | Exception3 e)
        catch后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个catch块来处理异常。
        在catch块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。与其它对象一样,可以访问一个异常对象的成员变量或调用它的方法。
            ①、getMessage() 获取异常信息,返回字符串。
            ②、printStackTrace() 获取异常类名和异常信息,以及异常出现在程序中的位置。返回值void。
        如果当前try块中发生的异常在后续的所有catch中都没捕获到,则先去执行finally,然后到这个函数的外部caller中去匹配异常处理器。
        如果try中没有发生异常,则所有的catch块将被忽略。
        注意:如果明确知道产生的是何种异常,可以用该异常类作为catch的参数;也可以用其父类作为catch的参数。比如:可以用 ArithmeticException 类作为参数的地方,就可以用RuntimeException类作为参数,或者用所有异常的父类Exception类作为参数。但不能是与ArithmeticException类无关的异常,如NullPointerException(catch中的语句将不会执行)。
 
    finally:
        finally块通常是可选的。捕获异常的最后一步是通过finally语句为异常处理提供一个统一的出口,使得在控制流转到程序的其它部分以前,能够对程序的状态作统一的管理。
        不论在try代码块中是否发生了异常事件,catch语句是否执行,catch语句是否有异常,catch语句中是否有return,finally块中的语句都会被执行。
        一个try至少要有一个catch块,否则, 至少要有1个finally块。但是finally不是用来处理异常的,finally不会捕获异常。
        finally主要做一些清理工作,如流的关闭,数据库连接的关闭等。 

8.7.2 Java异常处理机制2: throws结构:

Java异常类对象除在程序执行过程中出现异常时由系统自动生成并抛出,也可根据需要使用人工创建并抛出。首先要生成异常类对象,然后通过throw语句实现抛出操作(提交给Java运行环境)。
throw exceptionObject 程序员也可以通过throw语句手动显式的抛出一个异常。throw语句的后面可以抛出的异常必须是Throwable或其子类的实例。throw 语句必须写在函数中,执行throw 语句的地方就是一个异常抛出点,它和由JRE自动形成的异常抛出点没有任何差别。

抛出的异常可以被调用这个方法的方法所捕获,多用于层层嵌套的调用方法的中间环节.比如shiro验证过程中,JWTFilter使用了myRealm进行token的验证,而如果验证出现问题,myRealm会抛出异常,然后会被调用它的JWTFilter所捕获,从而进行下一步的操作.

8.7.3 Springboot异常处理:

Springboot对异常的处理内置了多种方式,比较好用的是使用全局异常处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;


//方法增强,只有@RsetController注解下的方法才会调用这个异常处理模式
@RestControllerAdvice(annotations = RestController.class)


//这个注解开启日志功能
@Slf4j

//这个注解注入到spring容器
@Component

public class GlobalExpectionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    //处理参数效验的异常
    public Result handMethodArgumentNotValidException(MethodArgumentNotValidException e){
        
        //把异常对象e的message变成字符串,在日志上输出
        log.error("参数效验异常!",e);
        
        //给前端返回结果
        return new Result(401,e.getMessage(),null);
    }
    
}

8.8 :shiro+jwt实现权限验证:

8.8.1: shiro介绍:

shiro是一个java的安全框架,用于身份验证,授权,密码,会话管理等功能.能储存用户的登录凭证:session等,同时使用过滤器(filter)

对每个http请求进行过滤(根据接口和权限等信息限制访问).

shiro由三大部分组成:

(1)subject:当前操作的用户

(2)securitymanager:管理内部组件的实例,提供认证和授权的服务.

(3)realm:领域,是shiro和数据之间的连接器,相当与框架里的数据库.

8.8.2: jwt介绍:

jwt是json web token的缩写,是目前最流行的跨域身份认证解决方案.本质上是一个特殊的字符串.

由三部分组成:header(头部),payload(负载),signature(签名).

(1): header:

{

“alg”: 表明签名所使用的算法,一般为hs256.

“typ”: 令牌的类型,一般为”JWT”

}

(2): payload:

这七个字段都是可选的,并没有个数限制.

{

“iss”: 发行

“exp”: 到期时间

“sub”: 主题

“aud”: 用户

“nbf”: 在此之前不可以用

“iat”: 发布时间

“jti”: JWT id

}

这两个部分用base64算法变成两个字符串,由于算法是已知的,故任何人都会对其进行解密,所以不能在里面存储敏感的信息.

(3): signature:签名哈希

生成这部分需要(1)和(2)部分的两个字符串,还需要一个秘钥,储存在服务器中.通过(1)中指定的签名算法,生成第三部分.

第三部分的作用是检验token有没有被篡改.

由于哈希是不可逆的,所以真正进行验证时,会用算法解密前两部分,然后用得到的算法和存储在服务器端的秘钥重新进行加密得到第三部分,然后与客户端所传来的数据进行对比,如果相同就验证成功,如果不同就验证失败.

8.8.3: cookie,session和token:

(1)首先我们要知道,网页有静态网页和动态网页之分.静态网页由html编写,网页上所有的内容都已经被指定好了,不能灵活的显示内容,无法根据传入的参数实现动态的变化.而动态网页可以动态的解析参数,从而呈现不同的内容.

(2)无状态http:

一般的网页是由客户端发送请求给服务器,服务器解析请求后返回客户端,中间浏览器不会记录htpp的状态,从而导致很多时候出现浪费资源的问题,比如相应内容的重复问题,导致资源的浪费,为了解决这个问题,就开发出了可以维持浏览器状态的技术.

(3)session和cookie:

session:回话,储存在服务器端.

cookie:储存在客户端本地的数据

当客户端第一次请求服务器时,服务器解析请求的同时会建立session,同时返回响应给客户端,客户端储存cookie保存在本地.

当客户端再次访问服务器时,客户端带着cookie请求发送给服务器,服务器根据cookie找到自己所维护的session,通过解析session判断客户端状态从而发送响应的数据.

但是这种机制有很多问题,首先是服务器要维护用户的session,浪费了部分的资源,而且当后端系统使用负载均衡设计时(同一个系统分布在多台不同的服务器上),若用户在a节点成功登陆,那么他的session存储在a中,当用户在一次登陆时被分配到b节点,而b节点没有存储用户的相关信息,那么用户只能重新登录,这种体验非常不好.所以,jwt应运而生.

(4)jwt处理机制:

当客户端第一次发送请求时,服务器会做出响应,然后把状态信息加密成一个字符串,这就是token,然后把这个token传给客户端,并由客户端本地保存.客户端以后发送请求时携带token,由服务器解析token,并得到相应的状态.然后返回响应的数据.

这种机制把”传递数据”变成了”传递方法”,巧妙地解决了cookie-session这种状态维持的方式所带来的种种问题.

8.8.4: 在springboot中使用shiro+jwt:

8.8.4.1: 导入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.18.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.9.0</version>
</dependency>

8.8.4.2: 建立JWTUtil类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.chen.internetplus.config;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.chen.internetplus.pojo.User;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JWTUtil {
    //token有效时长:毫秒
    private static final long EXPIRE=7*24*60*60*1000;
    //token的密钥
    private static final String SECRET="InternetPlus";


    public static String createToken(User user) throws UnsupportedEncodingException {
        //token过期时间
        Date date=new Date(System.currentTimeMillis()+EXPIRE);

        //jwt的header部分
        Map<String ,Object>map=new HashMap<>();
        map.put("alg","HS256");
        map.put("typ","JWT");

        //使用jwt的api生成token
        String token= JWT.create()
                .withHeader(map)
                .withClaim("username", user.getUsername())//私有声明
                .withExpiresAt(date)//过期时间
                .withIssuedAt(new Date())//签发时间
                .sign(Algorithm.HMAC256(SECRET));//签名
        return token;
    }

    //校验token的有效性,1、token的header和payload是否没改过;2、没有过期
    public static boolean verify(String token){
        try {
            //解密
            JWTVerifier verifier=JWT.require(Algorithm.HMAC256(SECRET)).build();
            verifier.verify(token);
            return true;
        }catch (Exception e){
            return false;
        }
    }


    //无需解密也可以获取token的信息
    public static String getUsername(String token){
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }

    }
}

这个类用于处理token的各种操作.

8.8.4.3: 封装token

把token这个字符串封装成一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.chen.internetplus.config;

import org.apache.shiro.authc.AuthenticationToken;
//封装token
//shiro并不能够识别字符串的token(就是由JWTUtil中creatToken方法生成的),故要把这个字符串做成一个类
public class JWTToken implements AuthenticationToken {
    private String token;

    public  JWTToken(String token){
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

/**
 * @author: lhy
 * 自定义的shiro接口token,可以通过这个类将string的token转型成AuthenticationToken,可供shiro使用
 * 注意:需要重写getPrincipal和getCredentials方法,因为是进行三件套处理的,没有特殊配置shiro无法通过这两个方法获取到用户名和密码,需要直接返回token,之后交给JwtUtil去解析获取。(当然了,可以对realm进行配
置HashedCredentialsMatcher,这里就不这么处理了)
 */

8.8.4.4:编写JwtFilter过滤器:

用于筛选http请求,对不同的请求进行不同的操作.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package com.chen.internetplus.config;


//过滤器:用于更加精确地筛选http请求

import com.chen.internetplus.pojo.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.ExpiredCredentialsException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.stereotype.Component;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
//用这个注解把这个类注入到spring容器中
public class JWTFilter extends BasicHttpAuthenticationFilter {


    //这个方法的作用是判断请求头是否含有token
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        System.out.println("isLoginAttempt");
        HttpServletRequest req= (HttpServletRequest) request;
        String token=req.getHeader("Authorization");
        return token!=null;
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //判断请求的请求头是否带上 "Token"
        if (isLoginAttempt(request, response)) {
            String token = null;
            try {
                HttpServletRequest req = (HttpServletRequest) request;
                token = req.getHeader("Authorization");
                JWTToken jwt = new JWTToken(token);
                //交给自定义的realm对象去登录,如果错误他会抛出异常并被捕获
                getSubject(request, response).login(jwt);
                return true;
            } catch (ExpiredCredentialsException e) {
                Result result = Result.error(500,"Token认证失败!");
                try {
                    out(response, result);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                return false;
            } catch (AuthenticationException e) {
                Result result = Result.error(500,"Token错误!用户不存在!");
                try {
                    out(response, result);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                return false;
            }
        }
        //token不存在
        Result result = Result.error(500,"无token,请重新登录!");
        try {
            out(response,result);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }


    /**
     * json形式返回结果token验证失败信息,无需转发
     */
    private void out(ServletResponse response, Result res) throws IOException {
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        ObjectMapper mapper = new ObjectMapper();
        String jsonRes = mapper.writeValueAsString(res);
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json; charset=utf-8");
        httpServletResponse.getOutputStream().write(jsonRes.getBytes());
    }
}

8.8.4.5: 编写realm对象:

用于验证登录状态是否正确.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.chen.internetplus.config;

import com.chen.internetplus.pojo.User;
import com.chen.internetplus.service.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
//realm:是shiro两大功能:认证和授权的入口,通过自定义realm,对认证和授权功能进行自定义处理
public class MyRealm extends AuthorizingRealm{

    @Autowired
    UserService userService;

    //添加注解支持:限定这个realm只能处理JWTToken,不加的话会报错
    @Override
    public boolean supports(AuthenticationToken token){
        return token instanceof JWTToken;
    }
    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //从刚才封装的JWTToken中重新取出token
        String jwt= (String) token.getCredentials();

        //验证token
        if (!JWTUtil.verify(jwt)){
            throw new ExpiredCredentialsException("token认证失效,token错误或者过期,重新登陆");
        }

        //获取用户名
        String username= JWTUtil.getUsername(jwt);

        //获取用户
        User user=userService.getUser(username);
        if (user==null){
            throw new AuthenticationException("该用户不存在");
        }
    //对于获取不到对象的问题,网上搜索栏了很多种方法,这是其中一种,但未解决问题
        SecurityUtils.getSubject().getSession().setAttribute("User", user);
    //return new SimpleAuthenticationInfo(jwt,jwt,"MyRealm");
    //当把第一个参数从jwt变成user对象时,就可以获取当前用户了
        return new SimpleAuthenticationInfo(user,jwt,"MyRealm");
    }

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }
}

8.8.4.6: 编写shiro配置:

最终配置,调用所有组件的地方.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package com.chen.internetplus.config;

import com.chen.internetplus.config.JWTFilter;
import com.chen.internetplus.config.MyRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.Filter;


@Configuration
public class
ShiroConfig {


    //1.创建realm对象
    @Bean
    public MyRealm myRealm(){
        return new MyRealm();
    }

    //2.SecurityManager
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager securityManager(MyRealm myRealm){
        DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
        // 设置自定义 realm.
        securityManager.setRealm(myRealm);
        //关闭session
        DefaultSubjectDAO subjectDAO=new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator sessionStorageEvaluator=new DefaultSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }


    //创建ShiroFilter(用于拦截所有请求,对受限资源进行Shiro的认证和授权判断)
    //Shiro提供了丰富的过滤器(anon等),不过在这里就需要加入我们自定义的JwtFilter了
    //3.配置过滤器
    @Bean
    public ShiroFilterFactoryBean factory(@Qualifier("securityManager")DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean factoryBean=new ShiroFilterFactoryBean();
        //设置安全管理器
        factoryBean.setSecurityManager(securityManager);

        //设置我们自定义的JWT过滤器,并注册JWTFilter到ShiroFilterFactoryBean中
        Map<String, Filter> filterMap=new LinkedHashMap<>();
        filterMap.put("jwt",new JWTFilter());
        factoryBean.setFilters(filterMap);

        //配置系统的受限资源以及对应的过滤器
        Map<String, String> ruleMap = new HashMap<>();
        ruleMap.put("/activity/createActivity","jwt");
        ruleMap.put("/activity/myActivity","jwt");
        ruleMap.put("/activity/approveActivity","jwt");
        ruleMap.put("/activity/lottery","jwt");
        ruleMap.put("/activity/show","jwt");
        ruleMap.put("/activity/change","jwt");
        ruleMap.put("/activity/add","jwt");
        ruleMap.put("/user/getuser","jwt");
        ruleMap.put("/user/changenickname","jwt");
        ruleMap.put("/**","anon");
        factoryBean.setFilterChainDefinitionMap(ruleMap);
        return factoryBean;
    }

    /**
     * 开启对 Shiro 注解的支持
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

除了配置拦截器,可以拦截器全部放行,通过注解实现拦截:

在接口和Controller类上加@RequiresAuthentication注解:需要token

@RequiresRoles:授权

8.8.4.7: 获取当前登录对象

通过shiro框架可以根据token直接获得当前对象,不需要每次调用接口都传入用户名等信息.

SecurityUtils.getSubject().getPrincipal().

8.9: lombok:

8.9.1: 介绍:

在Java中,封装是一个非常好的机制,最常见的封装莫过于get,set方法了,无论是Intellij idea 还是Eclipse,都提供了快速生成get,set方法的快捷键,使用起来很是方便,其实,我们还有更方便的办法,就是今天的主角-Lombok.

简而言之,就是帮助我们快速生成重复代码的工具.

8.9.2: 导入依赖:

1
2
3
4
5
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.12</version>
</dependency>

8.9.3: 使用:

Lombok提供注解方式来提高代码的简洁性,常用注解有:

  • @Data

  • @Setter @Getter

  • @NonNull

  • @Synchronized

  • @ToString

  • @EqualsAndHashCode

  • @Cleanup

  • @SneakyThrows

下面重点介绍其中几个:

@Data:相当于同时添加@Setter @Getter,@ToString,@EqualsAndHashCode这些注解,一般在实体类上使用.

@Setter @Getter:自动为所有属性添加set.get方法.方法名遵循驼峰命名法.

@NonNull:该注解快速判断是否为空,如果为空,则抛出java.lang.NullPointerException.

@ToString:自动生成toString()方法:用String的形式去描述一个类

8.10: Slf4j:

使用这个注解,在控制台实现日志的输出.

使用方法:再类的最上方加上@Slf4j这个注解,然后在代码中直接使用log.方法()就可以在控制台输出日志了,非常的方便.

slf4j的日志级别分为五种:info、debug、error、warn、trance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
info:  一般处理业务逻辑的时候使用,就跟 system.err打印一样,用于说明此处是干什么的。slf4j使用的时候是可以动态的传参的,使用占位符 {} 。后边一次加参数,会挨个对应进去。

debug: 一般放于程序的某个关键点的地方,用于打印一个变量值或者一个方法返回的信息之类的信息

error: 用户程序报错,必须解决的时候使用此级别打印日志。
    
warn: 警告,不会影响程序的运行,但是值得注意。

trance: 一般不会使用,在日志里边也不会打印出来,好像是很低的一个日志级别。

举个例子:
在类上方添加:
@Slf4j
public class UserController {;}
使用时:
public Integer text(){
        log.info("这样使用日志.处理一般的业务逻辑,在控制台会显示绿色的info(information的缩写,表示提示信息)标识.");
        log.error("这是发生错误时的日志使用方式,在控制台会显示出红色的ERROR标识.);
        log.debug("这是使用debug的日志使用方式.");
        log.warn("这是使用警告的日志使用方式,在控制台会显示出黄色的WARN提示.);
}

9.项目实践:

9.1: 选课系统:

9.1.1: 介绍:

第一个springboot项目,作为werun实验室的考核大作业.

9.1.2: 技术:

9.1.2.1: find in set:

FIND_IN_SET(str,strlist)

str 要查询的字符串,strlist 字段名 参数以”,”分隔 如 (1,2,6,8),查询字段(strlist)中包含(str)的结果,返回结果为null或记录假如字符串str在由N个子链组成的字符串列表strlist 中,则返回值的范围在 1 到 N 之间。 一个字符串列表就是一个由一些被 ‘,’ 符号分开的子链组成的字符串。如果第一个参数是一个常数字符串,而第二个是type SET列,则FIND_IN_SET() 函数被优化,使用比特计算。 如果str不在strlist 或strlist 为空字符串,则返回值为 0 。如任意一个参数为NULL,则返回值为 NULL。这个函数在第一个参数包含一个逗号(‘,’)时将无法正常运行。

例子:

1
2
@Query(value = "SELECT * FROM Course WHERE FIND_IN_SET('已通过',status)")
List<Course> findAllByStatus(String status);

这个函数适用于这种情况:

比如一个字段是一个字符串列表: 1,2,3,还有另一个记录的该字段值是:1,22,33,若用like进行搜索,则会搜索到这两条记录,但是如果用这个函数就只能搜索到第一条记录,作为一种特殊的搜索方式.

9.1.2.2: 分页:

功能举例如下:

1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/administrator/query")
//教务功能:查看所有课程
//通过分页的方式呈现,不进行排序所以去掉sort参数
// pageNum是指查询页,默认从0开始
// pageSize是每页的期望行数,设置为10
public Page<Course> administratorQuery(@RequestParam(name = "pageNum", defaultValue = "0") Integer pageNum,
                         @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize){
PageRequest of = PageRequest.of(pageNum, pageSize);
    Page<Course> courses = courseRepository.findAll(of);
    return courses;
}

9.2: 云端笔记:

9.2.1: 介绍:

作为加入实验室后的考核项目,第一次与前端的同学进行合作和对接.

9.2.2: 技术:

9.2.2.1: 项目设计过程:

数据库表设计-实体类设计-服务层设计-接口设计(根据原型图)

9.2.2.2: 封装service:

写service层,在里面实现接口,同时写serviceimpl层进行接口的实现,这样做的好处是在项目需求发生变化时,只需要重新建立serviceimpl,不用修改所有的代码,符合开闭原则(程序对修改关闭,对增添开放)

具体操作:

(1):在service层中写接口:

public interface NoteService {

1
2
Result<String> addNote(String name, String username, String status, String describe, String content, String type);
Result<List<Note>> noteList(String username);

}

(2): 在serviceimpl类中实现service接口,同时加@service注解.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.example.serviceImp;

import com.example.dao.NoteRepository;
import com.example.dao.TypeRepository;
import com.example.entity.Note;
import com.example.entity.Type;
import com.example.service.NoteService;
import com.example.util.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

@Service
@Transactional
public class NoteServiceImp implements NoteService {

    @Autowired
    private NoteRepository noteRepository;

    @Autowired
    private TypeRepository typeRepository;

    @Override
    public Result<String> modifyNote(String name,String username,String status,String describe,String content,String type){
        //修改笔记
        try{
            //首先找到笔记
            Result<Note> result = note(username, type, name);
            Note note = result.getData();
            if(note == null){
                return new Result<String>(400,"修改笔记失败!笔记不存在",null);
            }
            //修改笔记
            note.setNotecontent(content);
            note.setNotedescribe(describe);
            note.setNotestatus(status);
            note.setNotetype(type);
            note.setNotename(name);
            note.setUsername(username);

            //重新获取时间
            SimpleDateFormat sdf = new SimpleDateFormat();
            sdf.applyPattern("yyyy-MM-dd");
            Date date = new Date();
            String data = sdf.format(date);
            note.setNotetime(data);

            //保存
            noteRepository.save(note);
            return new Result<String>(200,"修改笔记成功!",null);
        }catch(Exception e){
            return new Result<String>(500,"修改笔记失败!",null);
        }
    }

}

(3): 在controller类中调用时,注入接口类,内部会自动注入其实现类:

1
2
3
@Autowired
//写的是注入接口,事实上是注入了实现类
NoteService noteService;

9.3: 抗疫志愿者信息化应用平台:

9.3.1: 介绍:

参加互联网+创新创业大赛的项目,第一次设计项目,第一次领导项目.

9.3.2: 技术:

9.3.2.1:注解AOP:

通过注解自定义aop进行全局异常处理.再出现异常时,同时实现向前端的返回和向控制台的输出.

(1):自定义注解:

1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface BearLogger {
}

(2):自定义切面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.chen.internetplus.utils.manageResult;

import com.chen.internetplus.pojo.Result;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.HashMap;

@Aspect
@Component//注意一定要将此切面设入spring容器中,否则无法调用进行方法增强
public class MyPointcuts {
    @Pointcut("@annotation(BearLogger)")
    public void logBefore(){

    }
//    @Around("@annotation(BearLogger)")
//    通过这个注解进行环绕,这样在想使用这个的地方加上这个注解即可.
    @Around("execution(* com.chen.internetplus.controller.*.*(..))")
    //可以用 @Around("execution(* com.chen.studyaop.controllers.HelloController.*(..))")指定某个类下所有方法
    private Result bearLog(ProceedingJoinPoint joinPoint) {
        ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory();
        Logger logger = loggerFactory.getLogger("");
        try {

            String classType = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            // 参数
            Object[] args = joinPoint.getArgs();
            Class<?>[] classes = new Class[args.length];
            for (int k = 0; k < args.length; k++) {
                if (!args[k].getClass().isPrimitive()) {
                    // 获取的是封装类型而不是基础类型
                    String result = args[k].getClass().getName();
                    Class s = map.get(result);
                    classes[k] = s == null ? args[k].getClass() : s;
                }
            }
            ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
            // 获取指定的方法,第二个参数可以不传,但是为了防止有重载的现象,还是需要传入参数的类型
            Method method = Class.forName(classType).getMethod(methodName, classes);
            // 参数名
            String[] parameterNames = pnd.getParameterNames(method);
            // 通过map封装参数和参数值
            HashMap<String, Object> paramMap = new HashMap();
            for (int i = 0; i < parameterNames.length; i++) {
                paramMap.put(parameterNames[i], args[i]);
                logger.info("参数名:"+parameterNames[i]+"  参数值"+args[i]);
            }
            return (Result) joinPoint.proceed();
        } catch (Throwable e) {
            if (e instanceof GlobalException) {

                logger.error(e.getMessage());
                logger.error("执行方法调用失败,此失败来源于代码内手动抛出");
                return Result.error(e.getMessage());
            }
            logger.error(e.getMessage());
            logger.error("未排除失败来源于系统性失败");
            return Result.error("服务器端错误,来源于代码错误"+"错误内容如下"+e.getMessage());
        }
    }
    private static HashMap<String, Class> map = new HashMap<String, Class>() {
        {
            put("java.lang.Integer", int.class);
            put("java.lang.Double", double.class);
            put("java.lang.Float", float.class);
            put("java.lang.Long", Long.class);
            put("java.lang.Short", short.class);
            put("java.lang.Boolean", boolean.class);
            put("java.lang.Char", char.class);
        }
    };

}

目前并未完全理解这段代码,后续慢慢学习.

9.3.2.2: 生成不重复的随机数:

可以按照传入的数量生成一组随机数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.chen.internetplus.utils.manageResult;


import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Random;
public class MyRandom {

     public  int[] getTheRandomArray(int count) throws NoSuchAlgorithmException {
            //采用更随机的生成随机数的方式,生成任意数量个不重复的随机数
            SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
            int array[] = new int[count];
            //首先 Random类中的nextInt(int bound)方法是产生一个从0到bound-1的一个随机数
            //为了防止第一个数是0而无法放入,所以我选择不使用数组默认的初始化方式,而是选择让所有的数值都为bound值。
            for(int i=0;i<count;i++){
                array[i] = count;
            }
            //这是一个标志,当flag的值为false时,
            //说明新产生的随机数没有与数组当中的数值重复,跳出while循环。
            boolean flag = false;
            //这是一个for循环进行赋值
            for(int i=0;i<count;i++){
                //这个变量的作用有必要说明一下
                //我们的flag一开始是false值,为了保证我们的while循环第一次可以执行,我们设置了这样一个变量。
                int start = 0;
                int temp = count ;
                while(/**三目表达式,这个地方体现了start变量的价值*/start==0?true:flag){
                    //我们的测试循环很简单,一旦发现新生成的随机数和之前在数组中的随机数重复
                    //马上重新生成并重新进行比较
                    //当发现重复时我们会将flag设置为true值,这样while循环得以执行
                    //当开始下一次的while循环时,我们就需要将flag设置为false值
                    //这样是为了防止即使新产生的temp值并没有出现重复,由于flag依旧为true从而陷入死循环
                    flag = false;
                    temp = random.nextInt(count);
                    for(int j=0;j<i;j++){
                        if(temp ==array[j]){
                            flag = true;
                            break;
                        }
                    }
                    //一次while循环结束后,对start变量进行递增
                    start++;
                }
                //while循环执行结束后,说明新产生的temp随机变量并不会重复,将它放在数组中即可
                array[i]  = temp;
            }
            return array;
        }
}

9.3.2.3: 获取时间和时间比较:

通过calendar进行比较.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//获取当前时间,转换为calendar
Date d = new Date();
SimpleDateFormat time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Calendar nowTime = Calendar.getInstance();
nowTime.setTime(time.parse(time.format(d)));

//获取招募时间段,转换为calendar
Activity activity = activityRepository.findByTitle(title);
String start = activity.getStarttime();
String end = activity.getEndtime();
Date start1 = time.parse(start);
Date end1 = time.parse(end);
Calendar startTime = Calendar.getInstance();
startTime.setTime(start1);
Calendar endTime = Calendar.getInstance();
endTime.setTime(end1);

//比较时间
if (nowTime.after(endTime) || nowTime.before(startTime)) {
    return Result.error(500, "报名失败!,不在报名时间段内!");
}

9.3.2.4: swagger-ui:

swagger-ui是一个可以自动为controller类的接口生成接口文档的组件,通过简单的配置自动生成接口文档,方便与前端的同学进行对接.

(1)配置swagger:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Configuration
@EnableSwagger2
public class SwaggerConfig extends WebMvcConfigurationSupport {
    @Bean
    public Docket createRestApi() {

        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("互联网加 Api")
                //swagger的标题
                .description("需要token的接口均为web前端操作部分")
                //swagger的提示
                .termsOfServiceUrl("")
                .contact(new Contact("Spring Cloud --> =.=#", "http://spring.io/projects/spring-boot", ""))
                .version("2.0")
                .build();
    }
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 解决静态资源无法访问
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/static/");
        // 解决swagger无法访问
        registry.addResourceHandler("/swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        // 解决swagger的js文件无法访问
        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

}

(2)在具体实体类上使用:

1
2
3
4
@ApiModelProperty("活动标题")
//加上这个注解,表明实体类的意义.
@Column(name = "title", precision = 255)
private String title;

(3) 在接口上使用:

1
2
3
4
5
6
7
8
9
@PostMapping("approveActivity")
@ApiOperation("通过活动id批准活动进行")
//这个注解表明接口的作用
public Result approveActivity(@ApiParam("活动标题") 
							//这个接口表明这个参数的意义。还可添加提示信息等.
						@RequestParam("title") String title,
                              @ApiParam("审批类型,默认为0未审批,1为审批通过,2为审批未通过") @RequestParam("type") Integer type){
    return activityService.approveActivity(title, type);
}

9.3.2.5: 图片处理:

大体思路是:上传图片后,不把图片存放进入数据库,那样效率太低,而是存进服务器.然后返回一个url,当需要这个图片时,访问这个url就可以得到这张图片.

9.3.2.5.1: 新建文件工具类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Base64;

public class FileUtil {

    private static String basePath = System.getProperty("user.dir");
    private static String s = File.separator;
    public static String generateFile(MultipartFile file, String uniqueString, String name) throws IOException {
        String ip_port = "3.36.73.0" + ":" + "8080" + "/file/getFIleByURL?code=";
        //这是要储存的公网的ip.
        if (file == null) {
            throw new IOException("未选择文件");
        }
        String type = file.getContentType();
        switch (type) {
            case "image/jpeg":
                type = ".jpg";
                break;
            case "image/gif":
                type = ".gif";
                break;
            case "image/png":
                type = ".png";
                break;
            default:
                throw new IOException("不接受的文件类型,只接受jpg,gif,png格式");
        }
        File newFile = new File(basePath + s + "data" + s + name + s + uniqueString + s + "ininame"+type);
        if (newFile.exists()) {
            newFile.delete();
        } else {
            newFile.mkdirs();
        }
        newFile.createNewFile();
        file.transferTo(newFile);
        Base64.Encoder encoder = Base64.getEncoder();
        String code = encoder.encodeToString(newFile.getAbsolutePath().getBytes(Charset.forName("GBK")));
        return ip_port + code;
    }
}
9.3.2.5.2: 新建上传图片实现类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Result uploadUserPicture(MultipartFile file,String username);
@Override
   public Result uploadUserPicture(MultipartFile file, String username) {
       String url;
       try{
           url = FileUtil.generateFile(file,username,"personImg");
           User user = userRepository.findUserByUserName(username);
           user.setUrl(url);
           userRepository.save(user);
       }catch (IOException e){
           return Result.error(400,e.getMessage());
       }
       return Result.success("文件上传成功:",url);
}
9.3.2.5.3: 新建上传图片接口:
1
2
3
4
5
@PostMapping("/user/uploadUserPicture")
   @ApiOperation("用户上传头像,上传后会生成一串路径存在用户信息中属性名url,直接访问路径即可得到图片")
   public Result uploadUserPicture(@RequestBody MultipartFile file, @RequestParam String username) {
       return userService.uploadUserPicture(file, username);
   }

图片上传后,可以选择情况把图片的url储存到数据库或者其他方案.

9.3.2.5.4: 新建获取图片接口:

按照上文的url访问这个接口,可以获取图片.

1
2
3
4
5
6
7
8
9
10
11
@GetMapping(value = "/file/getFIleByURL",produces = MediaType.IMAGE_JPEG_VALUE)
 @ApiOperation("统一获取图片接口")
 public byte[] getMyFIle(@RequestParam String code) throws IOException {
     Base64.Decoder decoder = Base64.getDecoder();
     code = code.replaceAll(" ", "+");
     String url = new String(decoder.decode(code));
     System.out.println("url:" + url);
     BufferedImage bufferedImage = ImageIO.read(new FileInputStream(url));
     ByteArrayOutputStream out = new ByteArrayOutputStream();
     ImageIO.write(bufferedImage, "png", out);
     return out.toByteArray();

}

9.3.2.5.5: 文件上传优化:

原先的代码没有经过优化和测试,现在在经过测试和学习之后,优化过的文件上传代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package com.controller;

import com.pojo.User;
import com.repository.UserRepository;
import com.service.UserService;
import com.util.Result;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Base64;

@Slf4j
@RestController
@CrossOrigin(origins = "*")
@RequestMapping("user")
public class UserController {



    @Autowired
    UserService userService;

    @Autowired
    UserRepository userRepository;

    @PostMapping("/upPicture")
    @ApiOperation("用户上传头像,上传后会生成一串路径存在用户信息中属性名url,直接访问路径即可得到图片")
    public Result uploadUserPicture(@RequestBody MultipartFile file) {
//      User user = userRepository.findUserByUserName(username);
        try{

            //通过这个方法获得当前目录.相对路径
            String basePath = System.getProperty("user.dir");

            //因为不同操作系统下分隔符不同,所以通过这个方法获取当前系统的分隔符
            //File类中的 public static final String separator. 类的静态成员和静态方法可以直接访问,而不需要实例化.所以可以将很多工具类里面的方法设置为静态方法.
            String s = File.separator;

            String ip_port = "localhost" + ":" + "8080" + "/file/getFIleByURL?code=";
            if (file == null) {
                    throw new IOException("未选择文件");
            }

            //获取文件原名
            String name = file.getOriginalFilename();

            //获取文件的类型
            String type = file.getContentType();
            switch (type) {
                case "image/jpeg":
                    break;
                case "image/gif":
                    break;
                case "image/png":
                    break;
                default:
                    throw new IOException("不接受的文件类型,只接受jpg,gif,png格式");
            }
//            //以pathname为路径创建file对象
//            //创建的file可能是文件 也可能是文件夹,取决于后面的方法
//            //进入路径的逻辑是到文件为止,比如要在某个文件夹下创建文件,这个路径就要精确到要创建的那个文件的名字.与Linux进入文件夹的方式不同.
//           File newFile = new File(basePath  + s +"用户图片");
//            //判断文件夹是否存在
//           if (!newFile.exists()) {
//            //共有mkdir和mkdirs两个方法,第一个方法如果路径中的文件夹不存在会报错,建立不了文件或文件夹,第二个方法会依次建立所有的父类.然后再建立文件夹.
//               newFile.mkdirs();
//           }
           File file1 = new File(basePath+ s+ "src" + s + "main" +  s  + "resources" + s + "static" + s + name);
           //创建文件
           file1.createNewFile();

           //写入文件
           file.transferTo(file1);
//           Base64.Encoder encoder = Base64.getEncoder();
//           String code = encoder.encodeToString(newFile.getAbsolutePath().getBytes(Charset.forName("GBK")));
//           String url =  ip_port + code;
//           user.setUrl(url);
//           userRepository.save(user);
            return Result.success("文件上传成功:",name);
        } catch (IOException e) {
            return Result.success("文件上传失败!:",e.getMessage());
        }
    }

}
9.3.2.5.6: 文件获取问题:

Spring Boot 默认将 / 的所有访问映射到以下目录:

1
2
3
4
classpath:/static
classpath:/public
classpath:/resources
classpath:/META-INF/resources

classpath可以简单地理解为文件存储的目录.

比如把图片0.jpg存放到src/main/resources/static文件夹下.然后使用http://localhost:端口名/0.jpg访问既可以得到该图片.

如果想自己配置自定义的资源访问映射,有两种方式,一是通过yml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/dbaddscore
    username: root
    password: 123456
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
  mvc:
    log-request-details: true
#   static-path-pattern: /image/**   #想要访问的路径
# web:
#   resources:
#      static-locations: file:C:\Users\wpy\Documents\Tencent Files\2669184984\FileRecv\Farewell Light\学习\项目\JAVA\校园二手交易及失物招领平台\用户图片  #图片存储的路径
server:
  port: 8080
debug: true
location:
  file:C:\Users\wpy\Documents\Tencent Files\2669184984\FileRecv\Farewell Light\学习\项目\JAVA\校园二手交易及失物招领平台\用户图片\

加#的即为相关配置.

还有一种方式是通过重写拦截器.代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.config;


import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@PropertySource("classpath:application.yml")
@Configuration
public class WebConfig implements WebMvcConfigurer {


	//引用上文配置中yml里的字段
    //引用yml字段的方式,避免在本地测试与服务器端运行的时候需要频繁修改内部代码的问题
    @Value("${location}")
    private String location;

    //访问不了 一直是404 不知什么原因
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 解决静态资源无法访问
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/static/");
        //第一个字段是网页访问的地址,第二个字段是存储的地址
        registry.addResourceHandler("/img/**").addResourceLocations("file:D:\\PIC\\");
        //一定要注意最后也要加斜线.
    }

}

但在测试中,均未能解决问题,尝试了诸多方法,都在代码里体现,但仍然一直404报错.

9.3.2.5.7:上传图片过大问题:

通过yml配置解决.

1
2
3
4
5
spring:
  servlet:
    multipart:
      max-file-size: 10MB  # 单个文件的大小
      max-request-size: 100MB  # 上传文件的总大小

9.4: 加分系统:

9.4.1: 介绍:

校团委的项目,自己的idea,实际应用的项目.