Spring学习笔记

1 篇文章 0 订阅
1 篇文章 0 订阅

SPRING 学习笔记


Spring 常用模块

Spring IOC

核心容器提供spring框架的基本功能,核心容器主要组件是BeanFactory
它是工厂模式的实现,BeanFactory使用控制反转IOC模式将应用程序的配置和依赖性规范与实际的应用程序代码分开

Spring上下文

spring上下文是一个配置文件,向spring框架提供上下文信息,spring上下文包括企业服务
例如JNDI EJB 电子邮件 国际化(i18n) 校验和调度功能

Spring AOP

通过配置管理特性,spring aop 直接将面向切面编程集成到了spring框架中,可以将一些通用任务
如安全、事物、日志等集中进行管理,提高了复用性和管理的便捷性

Spring DAO

sprin orm插入了若干个ORM框架,从而提供了ORM的对象关系工具包括
jdo、ibaties sql map,所有这些都遵循了spring的通用事物和DAO异常层次结构

SpringBean

spring 3 中为 Bean 定义了 5 中作用域,分别为 singleton(单例)、prototype(原型)、request、session 和 global session

Spring WEB模块

web上下文建立在应用程序上下文之上,为基于web的程序提供了上下文

Spring MVC框架

MVC是一个全功能的构建WEB应用程序实现,通过策略接口,MVC变成为高度可配置的
MVC容纳了大量视图技术,包括JSP、Velocity、Tiles、iText

Spring/SpringBoot常用注解

文章目录

1. @SpringBootApplication

这里先单独拎出@SpringBootApplication 注解说一下,虽然我们一般不会主动去使用它。

Guide 哥:这个注解是 Spring Boot 项目的基石,创建 SpringBoot 项目之后会默认在主类加上。

@SpringBootApplication
public class SpringSecurityJwtGuideApplication {
      public static void main(java.lang.String[] args) {
        SpringApplication.run(SpringSecurityJwtGuideApplication.class, args);
    }
}

我们可以把 @SpringBootApplication看作是 @Configuration@EnableAutoConfiguration@ComponentScan 注解的集合。

package org.springframework.boot.autoconfigure;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
		@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
   ......
}

package org.springframework.boot;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {

}

根据 SpringBoot 官网,这三个注解的作用分别是:

  • @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制
  • @ComponentScan: 扫描被@Component (@Service,@Controller)注解的 bean,注解默认会扫描该类所在的包下所有的类。
  • @Configuration:允许在 Spring 上下文中注册额外的 bean 或导入其他配置类

2. Spring Bean 相关

2.1. @Autowired

自动导入对象到类中,被注入进的类同样要被 Spring 容器管理比如:Service 类注入到 Controller 类中。

@Service
public class UserService {
  ......
}

@RestController
@RequestMapping("/users")
public class UserController {
   @Autowired
   private UserService userService;
   ......
}
2.2. @Component,@Repository,@Service, @Controller

我们一般使用 @Autowired 注解让 Spring 容器帮我们自动装配 bean。要想把类标识成可用于 @Autowired 注解自动装配的 bean 的类,可以采用以下注解实现:

  • @Component :通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用@Component 注解标注。
  • @Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。
  • @Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
  • @Controller : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。
2.3. @RestController

@RestController注解是@Controller和@ResponseBody的合集,表示这是个控制器 bean,并且是将函数的返回值直 接填入 HTTP 响应体中,是 REST 风格的控制器。

Guide 哥:现在都是前后端分离,说实话我已经很久没有用过@Controller。如果你的项目太老了的话,就当我没说。

单独使用 @Controller 不加 @ResponseBody的话一般使用在要返回一个视图的情况,这种情况属于比较传统的 Spring MVC 的应用,对应于前后端不分离的情况。@Controller +@ResponseBody 返回 JSON 或 XML 形式数据

关于@RestController@Controller的对比,请看这篇文章:@RestController vs @Controller

2.4. @Scope

声明 Spring Bean 的作用域,使用方法:

@Bean
@Scope("singleton")
public Person personSingleton() {
    return new Person();
}

四种常见的 Spring Bean 的作用域:

  • singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。
  • prototype : 每次请求都会创建一个新的 bean 实例。
  • request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。
  • session : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。
2.5. @Configuration

一般用来声明配置类,可以使用 @Component注解替代,不过使用@Configuration注解声明配置类更加语义化。

@Configuration
public class AppConfig {
    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl();
    }

}

3. 处理常见的 HTTP 请求类型

5 种常见的请求类型:

  • GET :请求从服务器获取特定资源。举个例子:GET /users(获取所有学生)
  • POST :在服务器上创建一个新的资源。举个例子:POST /users(创建学生)
  • PUT :更新服务器上的资源(客户端提供更新后的整个资源)。举个例子:PUT /users/12(更新编号为 12 的学生)
  • DELETE :从服务器删除特定的资源。举个例子:DELETE /users/12(删除编号为 12 的学生)
  • PATCH :更新服务器上的资源(客户端提供更改的属性,可以看做作是部分更新),使用的比较少,这里就不举例子了。
3.1. GET 请求

@GetMapping("users") 等价于@RequestMapping(value="/users",method=RequestMethod.GET)

@GetMapping("/users")
public ResponseEntity<List<User>> getAllUsers() {
 return userRepository.findAll();
}
3.2. POST 请求

@PostMapping("users") 等价于@RequestMapping(value="/users",method=RequestMethod.POST)

关于@RequestBody注解的使用,在下面的“前后端传值”这块会讲到。

@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody UserCreateRequest userCreateRequest) {
 return userRespository.save(user);
}
3.3. PUT 请求

@PutMapping("/users/{userId}") 等价于@RequestMapping(value="/users/{userId}",method=RequestMethod.PUT)

@PutMapping("/users/{userId}")
public ResponseEntity<User> updateUser(@PathVariable(value = "userId") Long userId,
  @Valid @RequestBody UserUpdateRequest userUpdateRequest) {
  ......
}
3.4. DELETE 请求

@DeleteMapping("/users/{userId}")等价于@RequestMapping(value="/users/{userId}",method=RequestMethod.DELETE)

@DeleteMapping("/users/{userId}")
public ResponseEntity deleteUser(@PathVariable(value = "userId") Long userId){
  ......
}
3.5. PATCH 请求

一般实际项目中,我们都是 PUT 不够用了之后才用 PATCH 请求去更新数据。

  @PatchMapping("/profile")
  public ResponseEntity updateStudent(@RequestBody StudentUpdateRequest studentUpdateRequest) {
        studentRepository.updateDetail(studentUpdateRequest);
        return ResponseEntity.ok().build();
    }

4. 前后端传值

掌握前后端传值的正确姿势,是你开始 CRUD 的第一步!

4.1. @PathVariable@RequestParam

@PathVariable用于获取路径参数,@RequestParam用于获取查询参数。

举个简单的例子:

@GetMapping("/klasses/{klassId}/teachers")
public List<Teacher> getKlassRelatedTeachers(
         @PathVariable("klassId") Long klassId,
         @RequestParam(value = "type", required = false) String type ) {
...
}

如果我们请求的 url 是:/klasses/{123456}/teachers?type=web

那么我们服务获取到的数据就是:klassId=123456,type=web

4.2. @RequestBody

用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且Content-Type 为 application/json 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用HttpMessageConverter或者自定义的HttpMessageConverter将请求的 body 中的 json 字符串转换为 java 对象。

我用一个简单的例子来给演示一下基本使用!

我们有一个注册的接口:

@PostMapping("/sign-up")
public ResponseEntity signUp(@RequestBody @Valid UserRegisterRequest userRegisterRequest) {
  userService.save(userRegisterRequest);
  return ResponseEntity.ok().build();
}

UserRegisterRequest对象:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRegisterRequest {
    @NotBlank
    private String userName;
    @NotBlank
    private String password;
    @NotBlank
    private String fullName;
}

我们发送 post 请求到这个接口,并且 body 携带 JSON 数据:

{"userName":"coder","fullName":"shuangkou","password":"123456"}

这样我们的后端就可以直接把 json 格式的数据映射到我们的 UserRegisterRequest 类上。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LYFZLq75-1617893998921)(F:/typora/imgs/@RequestBody.png)]

👉 需要注意的是:一个请求方法只可以有一个@RequestBody,但是可以有多个@RequestParam@PathVariable。 如果你的方法必须要用两个 @RequestBody来接受数据的话,大概率是你的数据库设计或者系统设计出问题了!

5. 读取配置信息

很多时候我们需要将一些常用的配置信息比如阿里云 oss、发送短信、微信认证的相关配置信息等等放到配置文件中。

下面我们来看一下 Spring 为我们提供了哪些方式帮助我们从配置文件中读取这些配置信息。

我们的数据源application.yml内容如下::

wuhan2020: 2020年初武汉爆发了新型冠状病毒,疫情严重,但是,我相信一切都会过去!武汉加油!中国加油!

my-profile:
  name: Guide哥
  email: koushuangbwcx@163.com

library:
  location: 湖北武汉加油中国加油
  books:
    - name: 天才基本法
      description: 二十二岁的林朝夕在父亲确诊阿尔茨海默病这天,得知自己暗恋多年的校园男神裴之即将出国深造的消息——对方考取的学校,恰是父亲当年为她放弃的那所。
    - name: 时间的秩序
      description: 为什么我们记得过去,而非未来?时间“流逝”意味着什么?是我们存在于时间之内,还是时间存在于我们之中?卡洛·罗韦利用诗意的文字,邀请我们思考这一亘古难题——时间的本质。
    - name: 了不起的我
      description: 如何养成一个新习惯?如何让心智变得更成熟?如何拥有高质量的关系? 如何走出人生的艰难时刻?
5.1. @value(常用)

使用 @Value("${property}") 读取比较简单的配置信息:

@Value("${wuhan2020}")
String wuhan2020;
5.2. @ConfigurationProperties(常用)

通过@ConfigurationProperties读取配置信息并与 bean 绑定。

@Component
@ConfigurationProperties(prefix = "library")
class LibraryProperties {
    @NotEmpty
    private String location;
    private List<Book> books;

    @Setter
    @Getter
    @ToString
    static class Book {
        String name;
        String description;
    }
  省略getter/setter
  ......
}

你可以像使用普通的 Spring bean 一样,将其注入到类中使用。

5.3. PropertySource(不常用)

@PropertySource读取指定 properties 文件

@Component
@PropertySource("classpath:website.properties")

class WebSite {
    @Value("${url}")
    private String url;

  省略getter/setter
  ......
}

更多内容请查看我的这篇文章:《10 分钟搞定 SpringBoot 如何优雅读取配置文件?》 。

6. 参数校验

数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。

JSR(Java Specification Requests) 是一套 JavaBean 参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们 JavaBean 的属性上面,这样就可以在需要校验的时候进行校验了,非常方便!

校验的时候我们实际用的是 Hibernate Validator 框架。Hibernate Validator 是 Hibernate 团队最初的数据校验框架,Hibernate Validator 4.x 是 Bean Validation 1.0(JSR 303)的参考实现,Hibernate Validator 5.x 是 Bean Validation 1.1(JSR 349)的参考实现,目前最新版的 Hibernate Validator 6.x 是 Bean Validation 2.0(JSR 380)的参考实现。

SpringBoot 项目的 spring-boot-starter-web 依赖中已经有 hibernate-validator 包,不需要引用相关依赖。如下图所示(通过 idea 插件—Maven Helper 生成):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4dw9IgIg-1617893998923)(F:/typora/imgs/c7bacd12-1c1a-4e41-aaaf-4cad840fc073.png)]

非 SpringBoot 项目需要自行引入相关依赖包,这里不多做讲解,具体可以查看我的这篇文章:《如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!》。

👉 需要注意的是: 所有的注解,推荐使用 JSR 注解,即javax.validation.constraints,而不是org.hibernate.validator.constraints

6.1. 一些常用的字段验证的注解
  • @NotEmpty 被注释的字符串的不能为 null 也不能为空
  • @NotBlank 被注释的字符串非 null,并且必须包含一个非空白字符
  • @Null 被注释的元素必须为 null
  • @NotNull 被注释的元素必须不为 null
  • @AssertTrue 被注释的元素必须为 true
  • @AssertFalse 被注释的元素必须为 false
  • @Pattern(regex=,flag=)被注释的元素必须符合指定的正则表达式
  • @Email 被注释的元素必须是 Email 格式。
  • @Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @Size(max=, min=)被注释的元素的大小必须在指定的范围内
  • @Digits (integer, fraction)被注释的元素必须是一个数字,其值必须在可接受的范围内
  • @Past被注释的元素必须是一个过去的日期
  • @Future 被注释的元素必须是一个将来的日期
6.2. 验证请求体(RequestBody)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {

    @NotNull(message = "classId 不能为空")
    private String classId;

    @Size(max = 33)
    @NotNull(message = "name 不能为空")
    private String name;

    @Pattern(regexp = "((^Man$|^Woman$|^UGM$))", message = "sex 值不在可选范围")
    @NotNull(message = "sex 不能为空")
    private String sex;

    @Email(message = "email 格式不正确")
    @NotNull(message = "email 不能为空")
    private String email;

}

我们在需要验证的参数上加上了@Valid注解,如果验证失败,它将抛出MethodArgumentNotValidException

@RestController
@RequestMapping("/api")
public class PersonController {

    @PostMapping("/person")
    public ResponseEntity<Person> getPerson(@RequestBody @Valid Person person) {
        return ResponseEntity.ok().body(person);
    }
}
6.3. 验证请求参数(Path Variables 和 Request Parameters)

一定一定不要忘记在类上加上 Validated 注解了,这个参数可以告诉 Spring 去校验方法参数。

@RestController
@RequestMapping("/api")
@Validated
public class PersonController {

    @GetMapping("/person/{id}")
    public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5,message = "超过 id 的范围了") Integer id) {
        return ResponseEntity.ok().body(id);
    }
}

更多关于如何在 Spring 项目中进行参数校验的内容,请看《如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!》这篇文章。

7. 全局处理 Controller 层异常

介绍一下我们 Spring 项目必备的全局处理 Controller 层异常。

相关注解:

  1. @ControllerAdvice :注解定义全局异常处理类
  2. @ExceptionHandler :注解声明异常处理方法

如何使用呢?拿我们在第 5 节参数校验这块来举例子。如果方法参数不对的话就会抛出MethodArgumentNotValidException,我们来处理这个异常。

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    /**
     * 请求参数异常处理
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, HttpServletRequest request) {
       ......
    }
}

更多关于 Spring Boot 异常处理的内容,请看我的这两篇文章:

  1. SpringBoot 处理异常的几种常见姿势
  2. 使用枚举简单封装一个优雅的 Spring Boot 全局异常处理!

8. JPA 相关

8.1. 创建表

@Entity声明一个类对应一个数据库实体。

@Table 设置表名

@Entity
@Table(name = "role")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String description;
    省略getter/setter......
}
8.2. 创建主键

@Id :声明一个字段为主键。

使用@Id声明之后,我们还需要定义主键的生成策略。我们可以使用 @GeneratedValue 指定主键生成策略。

1.通过 @GeneratedValue直接使用 JPA 内置提供的四种主键生成策略来指定主键生成策略。

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

JPA 使用枚举定义了 4 中常见的主键生成策略,如下:

Guide 哥:枚举替代常量的一种用法

public enum GenerationType {

    /**
     * 使用一个特定的数据库表格来保存主键
     * 持久化引擎通过关系数据库的一张特定的表格来生成主键,
     */
    TABLE,

    /**
     *在某些数据库中,不支持主键自增长,比如Oracle、PostgreSQL其提供了一种叫做"序列(sequence)"的机制生成主键
     */
    SEQUENCE,

    /**
     * 主键自增长
     */
    IDENTITY,

    /**
     *把主键生成策略交给持久化引擎(persistence engine),
     *持久化引擎会根据数据库在以上三种主键生成 策略中选择其中一种
     */
    AUTO
}

@GeneratedValue注解默认使用的策略是GenerationType.AUTO

public @interface GeneratedValue {

    GenerationType strategy() default AUTO;
    String generator() default "";
}

一般使用 MySQL 数据库的话,使用GenerationType.IDENTITY策略比较普遍一点(分布式系统的话需要另外考虑使用分布式 ID)。

2.通过 @GenericGenerator声明一个主键策略,然后 @GeneratedValue使用这个策略

@Id
@GeneratedValue(generator = "IdentityIdGenerator")
@GenericGenerator(name = "IdentityIdGenerator", strategy = "identity")
private Long id;

等价于:

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

jpa 提供的主键生成策略有如下几种:

public class DefaultIdentifierGeneratorFactory
		implements MutableIdentifierGeneratorFactory, Serializable, ServiceRegistryAwareService {

	@SuppressWarnings("deprecation")
	public DefaultIdentifierGeneratorFactory() {
		register( "uuid2", UUIDGenerator.class );
		register( "guid", GUIDGenerator.class );			// can be done with UUIDGenerator + strategy
		register( "uuid", UUIDHexGenerator.class );			// "deprecated" for new use
		register( "uuid.hex", UUIDHexGenerator.class ); 	// uuid.hex is deprecated
		register( "assigned", Assigned.class );
		register( "identity", IdentityGenerator.class );
		register( "select", SelectGenerator.class );
		register( "sequence", SequenceStyleGenerator.class );
		register( "seqhilo", SequenceHiLoGenerator.class );
		register( "increment", IncrementGenerator.class );
		register( "foreign", ForeignGenerator.class );
		register( "sequence-identity", SequenceIdentityGenerator.class );
		register( "enhanced-sequence", SequenceStyleGenerator.class );
		register( "enhanced-table", TableGenerator.class );
	}

	public void register(String strategy, Class generatorClass) {
		LOG.debugf( "Registering IdentifierGenerator strategy [%s] -> [%s]", strategy, generatorClass.getName() );
		final Class previous = generatorStrategyToClassNameMap.put( strategy, generatorClass );
		if ( previous != null ) {
			LOG.debugf( "    - overriding [%s]", previous.getName() );
		}
	}

}
8.3. 设置字段类型

@Column 声明字段。

示例:

设置属性 userName 对应的数据库字段名为 user_name,长度为 32,非空

@Column(name = "user_name", nullable = false, length=32)
private String userName;

设置字段类型并且加默认值,这个还是挺常用的。

Column(columnDefinition = "tinyint(1) default 1")
private Boolean enabled;
8.4. 指定不持久化特定字段

@Transient :声明不需要与数据库映射的字段,在保存的时候不需要保存进数据库 。

如果我们想让secrect 这个字段不被持久化,可以使用 @Transient关键字声明。

Entity(name="USER")
public class User {

    ......
    @Transient
    private String secrect; // not persistent because of @Transient

}

除了 @Transient关键字声明, 还可以采用下面几种方法:

static String secrect; // not persistent because of static
final String secrect = “Satish”; // not persistent because of final
transient String secrect; // not persistent because of transient

一般使用注解的方式比较多。

8.5. 声明大字段

@Lob:声明某个字段为大字段。

@Lob
private String content;

更详细的声明:

@Lob
//指定 Lob 类型数据的获取策略, FetchType.EAGER 表示非延迟 加载,而 FetchType. LAZY 表示延迟加载 ;
@Basic(fetch = FetchType.EAGER)
//columnDefinition 属性指定数据表对应的 Lob 字段类型
@Column(name = "content", columnDefinition = "LONGTEXT NOT NULL")
private String content;
8.6. 创建枚举类型的字段

可以使用枚举类型的字段,不过枚举字段要用@Enumerated注解修饰。

public enum Gender {
    MALE("男性"),
    FEMALE("女性");

    private String value;
    Gender(String str){
        value=str;
    }
}
@Entity
@Table(name = "role")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String description;
    @Enumerated(EnumType.STRING)
    private Gender gender;
    省略getter/setter......
}

数据库里面对应存储的是 MAIL/FEMAIL。

8.7. 增加审计功能

只要继承了 AbstractAuditBase的类都会默认加上下面四个字段。

@Data
@AllArgsConstructor
@NoArgsConstructor
@MappedSuperclass
@EntityListeners(value = AuditingEntityListener.class)
public abstract class AbstractAuditBase {

    @CreatedDate
    @Column(updatable = false)
    @JsonIgnore
    private Instant createdAt;

    @LastModifiedDate
    @JsonIgnore
    private Instant updatedAt;

    @CreatedBy
    @Column(updatable = false)
    @JsonIgnore
    private String createdBy;

    @LastModifiedBy
    @JsonIgnore
    private String updatedBy;
}

我们对应的审计功能对应地配置类可能是下面这样的(Spring Security 项目):

@Configuration
@EnableJpaAuditing
public class AuditSecurityConfiguration {
    @Bean
    AuditorAware<String> auditorAware() {
        return () -> Optional.ofNullable(SecurityContextHolder.getContext())
                .map(SecurityContext::getAuthentication)
                .filter(Authentication::isAuthenticated)
                .map(Authentication::getName);
    }
}

简单介绍一下上面设计到的一些注解:

  1. @CreatedDate: 表示该字段为创建时间时间字段,在这个实体被 insert 的时候,会设置值

  2. @CreatedBy :表示该字段为创建人,在这个实体被 insert 的时候,会设置值

    @LastModifiedDate@LastModifiedBy同理。

@EnableJpaAuditing:开启 JPA 审计功能。

8.8. 删除/修改数据

@Modifying 注解提示 JPA 该操作是修改操作,注意还要配合@Transactional注解使用。

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {

    @Modifying
    @Transactional(rollbackFor = Exception.class)
    void deleteByUserName(String userName);
}
8.9. 关联关系
  • @OneToOne 声明一对一关系
  • @OneToMany 声明一对多关系
  • @ManyToOne声明多对一关系
  • MangToMang声明多对多关系

更多关于 Spring Boot JPA 的文章请看我的这篇文章:一文搞懂如何在 Spring Boot 正确中使用 JPA

9. 事务 @Transactional

在要开启事务的方法上使用@Transactional注解即可!

@Transactional(rollbackFor = Exception.class)
public void save() {
  ......
}

我们知道 Exception 分为运行时异常 RuntimeException 和非运行时异常。在@Transactional注解中如果不配置rollbackFor属性,那么事物只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事物在遇到非运行时异常时也回滚。

@Transactional 注解一般用在可以作用在或者方法上。

  • 作用于类:当把@Transactional 注解放在类上时,表示所有该类的public 方法都配置相同的事务属性信息。
  • 作用于方法:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息。

更多关于关于 Spring 事务的内容请查看:

  1. 可能是最漂亮的 Spring 事务管理详解
  2. 一口气说出 6 种 @Transactional 注解失效场景

10. json 数据处理

10.1. 过滤 json 数据

@JsonIgnoreProperties 作用在类上用于过滤掉特定字段不返回或者不解析。

//生成json时将userRoles属性过滤
@JsonIgnoreProperties({"userRoles"})
public class User {

    private String userName;
    private String fullName;
    private String password;
    @JsonIgnore
    private List<UserRole> userRoles = new ArrayList<>();
}

@JsonIgnore一般用于类的属性上,作用和上面的@JsonIgnoreProperties 一样。

public class User {

    private String userName;
    private String fullName;
    private String password;
   //生成json时将userRoles属性过滤
    @JsonIgnore
    private List<UserRole> userRoles = new ArrayList<>();
}
10.2. 格式化 json 数据

@JsonFormat一般用来格式化 json 数据。:

比如:

@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone="GMT")
private Date date;
10.3. 扁平化对象
@Getter
@Setter
@ToString
public class Account {
    @JsonUnwrapped
    private Location location;
    @JsonUnwrapped
    private PersonInfo personInfo;

  @Getter
  @Setter
  @ToString
  public static class Location {
     private String provinceName;
     private String countyName;
  }
  @Getter
  @Setter
  @ToString
  public static class PersonInfo {
    private String userName;
    private String fullName;
  }
}

未扁平化之前:

{
    "location": {
        "provinceName":"湖北",
        "countyName":"武汉"
    },
    "personInfo": {
        "userName": "coder1234",
        "fullName": "shaungkou"
    }
}

使用@JsonUnwrapped 扁平对象之后:

@Getter
@Setter
@ToString
public class Account {
    @JsonUnwrapped
    private Location location;
    @JsonUnwrapped
    private PersonInfo personInfo;
    ......
}
{
  "provinceName":"湖北",
  "countyName":"武汉",
  "userName": "coder1234",
  "fullName": "shaungkou"
}

11. 测试相关

@ActiveProfiles一般作用于测试类上, 用于声明生效的 Spring 配置文件。

@SpringBootTest(webEnvironment = RANDOM_PORT)
@ActiveProfiles("test")
@Slf4j
public abstract class TestBase {
  ......
}

@Test声明一个方法为测试方法

@Transactional被声明的测试方法的数据会回滚,避免污染测试数据。

@WithMockUser Spring Security 提供的,用来模拟一个真实用户,并且可以赋予权限。

    @Test
    @Transactional
    @WithMockUser(username = "user-id-18163138155", authorities = "ROLE_TEACHER")
    void should_import_student_success() throws Exception {
        ......
    }

IOC

依赖注入

让调用类对某一接口实现类的依赖关系由第三方(容器或协作类)注入,以移除调用类对某一接口实现类的依赖

注入方式

  • 构造函数注入
  • 属性注入
  • 工厂方法注入
  • 接口注入(同属性注入,Spring不支持)

实现原理

  • 使用Java反射技术
  • 反射相关类
    • ​ ClassLoader
    • ​ Class
    • ​ Constructor
    • ​ Method
    • ​ Field
  • 类装载器
    • 工作机制

      • 装载:查找和导入Class文

      • 链接

        • 检验
        • 准备
        • 解析
      • 初始化:对类的静态变量、静态代码块执行初始化工作

    • ClassLoader

      • ClassLoader(根装载器):装载JRE目录下的核心类库

      • ExtClassLoader(扩展类装载器):装载JRE扩展目录ext的JAR类包

      • AppClassLoader(系统类装载器):装载Classpath路径下的类包

SpringBean

Bean作用域
  1. singleton
    1. singleton :单例模式 (多线程下不安全)

      • singleton:单例模式,Spring IoC 容器中只会存在一个共享的 Bean 实例,无论有多少个

        Bean 引用它,始终指向同一对象。该模式在多线程下是不安全的。Singleton 作用域是

        Spring 中的缺省作用域,也可以显示的将 Bean 定义为 singleton 模式,配置为:

  2. prototype
    1. prototype: 原型模式 每次使用时创建

      • prototype:原型模式,每次通过 Spring 容器获取 prototype 定义的 bean 时,容器都将创建

        一个新的 Bean 实例,每个 Bean 实例都有自己的属性和状态,而 singleton 全局只有一个对

        象。根据经验,对有状态的bean使用prototype作用域,而对无状态的bean使用singleton

        作用域。

  3. request(Web环境特有)
    1. Request :一次 request 一个实例

      • request:在一次 Http 请求中,容器会返回该 Bean 的同一实例。而对不同的 Http 请求则会

        产生新的 Bean,而且该 bean 仅在当前 Http Request 内有效,当前 Http 请求结束,该 bean

        实例也将会被销毁。

  4. session(Web环境特有)
    • session:在一次 Http Session 中,容器会返回该 Bean 的同一实例。而对不同的 Session 请

      求则会创建新的实例,该 bean 实例仅在当前 Session 内有效。同 Http 请求相同,每一次

      session 请求创建新的实例,而不同的实例之间不共享属性,且实例仅在自己的 session 请求

      内有效,请求结束,则实例将被销毁。

  5. global session(Web环境特有)
    1. global Session:在一个全局的 Http Session 中,容器会返回该 Bean 的同一个实例,仅在

      使用 portlet context 时有效。

控制反转

IOC 叫做控制反转,指的是通过Spring 来管理对象的创建、配置、和生命周期

相当于把控制权交给了spring,不需要人工来管理对象之间复杂的依赖关系

好处就是解耦 使用HashMap来进行存储

底层原理

1、简单的一句话概括:工厂+反射+配置文件

2、详情

​ 2.1 xml解析 从xml配置文件中获取类的全路径名 <bean id = “userDao" class="com.xxx.UserDao"/>

​ 2.2 利用反射机制创建对象

Class clazz = Class.forName(“类的全路径名”)

Object object = clazz.newInstance();

​ 2.3 利用工厂模式将对象返回(IOC容器的底层就是对象工厂)


两种IOC核心容器

BeanFactory
  1. BeanFactory是 Spring框架的基础设施,面向Spring本身;

    ApplicationContext面向使用Spring框架的开发者,几乎所有的应用场合我们都直接使用ApplicationContext而非底层的BeanFactory。

  2. BeanFactory顶层接口

    1. 位于类结构树的顶端,它主要的方法就是getBean(String beanName) 该方法从容器中返回特定名称的Bean BeanFactory的功能通过其他的接口得到不断扩展
  3. SingletonBeanResitry 运行期间注册单例Bean

    定义了允许期间向容器注册单例Bean的方法;对于单实例(singleton)的Bean来说,BeanFactory会缓存Bean实例,所以第二次使用getBean获取Bean时将直接从IoC容器的缓存中获取Bean实例,Spring在DefaultSingletonBeanResitory类中提供一个用于缓存单实例Bean的缓存器,它是一个用HashMap实现的缓存器

  4. 依赖日志管理插件

    在初始化BeanFactory时,必须为其提供一种日志框架,比如使用Log4j,slf4j,这样在启动时才不会报错

  5. 顶层接口BeanFactory和ApplicationContext的区别

  6. image-20210404233556607

    • 单例对象适用
      • ApplicationContext在构建核心容器时,创建对象采用的是立即加载的方式,即配置文件一读取完,立马创建对象
    • 多例对象适用
      • BeanFactory在构建核心对象时采用的是延迟加载的方式,即什么时候通过Id获取对象,什么时候创建
ApplicationContext
ApplicationContext常用实现类
  1. ClassPathXmlApplicationContext 读取类路径下的配置文件
  2. FileSystemXmlApplicationContext 读取电脑任意位置的配置文件
  3. AnnotationConfigApplicationContext 用于新注解+配置类的方式替代xml配置文件时
WebApplication体系架构

​ 是专门为web应用准备的,它允许从相对Web根目录的路径中装载配置文件完成初始化工作。从WebApplicationContext中可以获得ServletContext的引用,整个Web应用上下文对象将作为属性放置到ServletContext中,以便Web应用环境可以访问Spring上下文应用。

Spring 依赖注入 DI 四种方式

构造器注入
  1. ​ 必须要有构造器
  2. ​ bean标签下标签
    1. name:属性名
    2. value:值,针对基本类型和String
    3. ref:针对对象类型
/*带参数,方便利用构造器进行注入*/
public  CatDaoImpl(String message){
    this.message=message;
}
<bean id="CatDaoImpl" class="co m.CatDaoImpl">
    <constructor-arg value="message"></constructor-arg>
    </bean>
setter方法
  1. 条件:属性必须要有setter方法
  2. bean标签下标签
    1. name:属性名
    2. value:属性值,针对基本属性和String类型
    3. ref:针对对象类型,指向的是bena标签的id属性的值
public class Id{
    private int id;
    public int getId(){
        return id;
    }
    public void setId(int Id){
        this.id = id;
    }
    <bean id="id" class="com.id"><property name ="id" value = "123"></property></bean>
}
静态工厂注入
  1. 静态工厂:顾名思义,就是通过调用静态工厂的方法来获取自己需要的对象,为了让spring管理所有对象,我们不能直接通过**工程类.静态方法()**来获取对象,而是依然通过spring注入的方式获取
  2. 条件:需要工厂类,该工厂类中需要有静态方法
    1. 配置文件语法 <bean id = factory class =“com.zzub.factory.BeanFactory" factory-method=“静态方法名”/>
    2. 当用spring容器调用getBean这个方法时,会创建工厂类对象,并执行工厂类中的方法返回相应的对象并放入spring容器中。
public class DaoFactory { //静态工厂

public static final FactoryDao getStaticFactoryDaoImpl(){

return new StaticFacotryDaoImpl();

}

}

public class SpringAction {

private FactoryDao staticFactoryDao; //注入对象

//注入对象的 set 方法

public void setStaticFactoryDao(FactoryDao staticFactoryDao) {

this.staticFactoryDao = staticFactoryDao;

}

}

//factory-method="getStaticFactoryDaoImpl"指定调用哪个工厂方法

<bean name="springAction" class=" SpringAction" >

<!--使用静态工厂的方法注入对象,对应下面的配置文件-->

<property name="staticFactoryDao" ref="staticFactoryDao"></property>

</bean>

<!--此处获取对象的方式是从工厂类中获取静态方法-->

<bean name="staticFactoryDao" class="DaoFactory"

factory-method="getStaticFactoryDaoImpl"></bean>
实例工厂

实例工厂的意思是获取对象实例的方法不是静态的,所以你需要首先 new 工厂类,再调用普通的

  1. 条件:需要工厂类,这个工厂类中需要有普通方法
  2. 配置文件语法
    1. 需要先创建工厂对象再调用工厂中的普通方法。

实例方法

public class DaoFactory { //实例工厂

public FactoryDao getFactoryDaoImpl(){

return new FactoryDaoImpl();
}

}

public class SpringAction {

private FactoryDao factoryDao; //注入对象

public void setFactoryDao(FactoryDao factoryDao) {

this.factoryDao = factoryDao;

}

}

<bean name="springAction" class="SpringAction">

<!--使用实例工厂的方法注入对象,对应下面的配置文件-->

<property name="factoryDao" ref="factoryDao"></property>

</bean>

<!--此处获取对象的方式是从工厂类中获取实例方法-->

<bean name="daoFactory" class="com.DaoFactory"></bean>

<bean name="factoryDao" factory-bean="daoFactory"

factory-method="getFactoryDaoImpl"></bean>

IOC 开发时较常用的方式

全自动配置方式
  1. 基本不用配置文件
  2. 在类上使用@Component注解标记 类中的依赖关系使用@Autowired注解标记
  3. 使用@MapperScan注解
  4. **注意,如果一个类型匹配到了多个实现类,那么必须去使用@Qualifier注解指明需要的类
  5. 四个创建对象的注解
    1. @Component 无法划分类的时候
    2. @Repository 一般用户标注Dao(Mapper)层 或者说标注数据库操作层
    3. @Service 业务层
    4. @Controller 控制层/表现层
  6. 三个依赖注入时的注解
    1. @Value 针对基本数据类型和String类型
    2. @Autowired
      1. 该注解由Spring框架提供
      2. 按类型去寻找,如果找不到那就是没有,如果找到多个就报错,解决方式就是搭配@Qualifier注解指明使用哪一个注解
    3. @Resource
      1. 该注解由JDK提供
      2. 先按照名字去找,第一种是name属性配置名字@Resource(name=“名字”) ,如果没有指定名字,则把变量名当作要寻找的属性名,如果再找不到,最后按照类型去找。

AOP

面向切面

  • ​ AOP叫做面向切面编程,目的是在不修改原有代码的基础上,把新的功能植入到程序中,提高代码的模块性

  • SpringAOP基于动态代理的方式实现,如果是接口的话就会使用JDK动态代理,反之使用CGLIB

    • JDK动态代理: 实现接口,java反射机制生成一个代理接口的匿名类 ,调用具体方法的时候调用invokeHandler
    • CGLIB 基于ClassLoad装载,修改字节码生成子类去处理

    好处解耦!

  • 底层原理-动态代理

    • 静态代理与动态代理的区别?
      1. 静态代理每次增强的时候都会创建一个新类,代码看起来十分臃肿
      2. 静态代理在代理前就知道要代理的对象,动态代理运行时才直到
      3. 静态代理一般只能代理一个类,动态代理实现了接口的多个子类
    • JDK动态代理和CGLIB 动态代理的区别
      1. JDK动态代理要求被代理对象必须实现接口,基于反射的机制实现,生成一个实现同样接口的一个代理类,然后通过重写方法的方式实现,实现对代码的增强
      2. 如果被代理类没有实现接口则使用CGLIB动态代理,基于ASM 第三方框架,通过修改字节码生成一个子类,然后重写父类的方法,实现对代码的增强
      3. 1.8之前CGLIB性能高一些,但1.8之后性能差不多

适用场景

  1. 性能检测
  2. 访问控制
  3. 事务管理
  4. 日志记录
  5. 权限控制
  6. 分布式追踪
  7. 缓存控制

术语

连接点(JoinPoint)

​ 程序执行过程中明确的点,一般是方法的调用。被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器

目标类(Target)

​ 包含连接点的对象。也被称作被通知或被代理对象。POJO

代理(Proxy)

​ AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类。

切入点(PointCut)

​ 就是带有通知的连接点,在程序中主要体现为书写切入点表达式

通知(Advice)

​ AOP在特定的切入点上执行的增强处理,有before(前置),after(后置),afterReturning(最终),afterThrowing(异常),around(环绕)

切面(Aspect)

​ 通常是一个类,里面可以定义切入点和通知

织入(Weaving)

​ 将切面应用到目标对象并导致代理对象创建的过程

引入(Introduction)

​ 在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段

注解(AspectJ框架)

  • @Aspect 执行一个类为切面类
  • @Before 前置一个通知,目标执行方法前执行
  • @After 后置通知,目标方法指定后执行
  • @AfterReturning 返回后通知,目标方法执行成功之后执行
  • @AfterThrowing 异常通知,目标方法出现异常执行
  • @Around 环绕通知,环绕目标方法执行
  • @Pointcut 定义一个切入点表达式用于其他注解引用
  • @Order(1) 标记增强类的优先即,数值越小优先级越小

Sping事务

在MySQL中就存在事务的概念,事务就是指一组sql语句的集合,集合中有许多条sql语句,可能是insert,update,select,delete,我们希望这些sql语句都能执行成功,或者执行失败,这些行为是一致的,作为一整个整体执行

四个特性 ACID

  • 原子性(Atomic):表明组成一个事务的多个数据库操作是一个不可分割的原子单元
  • 一致性(Consistency):事务操作成功后,数据库所处的状态和它的业务规则是一致的,即数据不会被破坏
  • 隔离性(Isolation): 在并发数据库操作时,不同的事务拥有各自的数据空间,他们的操作不会对对方产生干扰
  • 持久性(Durability): 一旦事务提交成功后,事务中所有的数据操作都必须被持久化到数据库中

事务隔离级别

1、read_uncommited(最高并发性和吞吐量)
2、read_commited
3、repeatable_read
4、serializable(最低并发性)

事务传播行为

propagation:传播行为(7种)—@Transational(propagation=Propagation.REQUIRED)
  • REQUIERD(默认)如果当前没有事务,就新建一个事务,如果当前存在一个事务,则加入该事务(最常用)

  • REQUIRES_NEW 无论当前是否有事务,都新建事务执行,如果当前已经在事务中,则把当前事务挂起 【单独开启事务2执行,失败了就回滚自己 事务1失败了不会影响到事务2】

  • SUPPORTS 如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行

  • NOT_SUPPORTS 强制要求以非事务方式执行,如果当前存在事务,就把当前事务挂起

  • MANDATORY 强制要求以事务方式执行 如果当前不存在事务,就抛出异常

  • NEVER 强制要求以非事务方式执行 如果存在事务,就抛出异常

  • NESTED 如果存在事务,则在嵌套事务内执行 如果当前没有事务,则按REQUIRED属性执行

    嵌套事务,外层的事务如果回滚,会导致内层的事务也回滚;
    但是内层的事务如果回滚,仅仅是回滚自己的代码

Spring声明式事务

底层原理式AOP
1、编程式

TransactionTemplate 手动编写提交回滚逻辑 | 对于业务代码具有侵入性

2、声明式(基于SpringAOP实现)
  1. XML式

    <!--配置事务管理器-->
    <bean id="transactionManager"
    class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    
    <!--结合aop实现事务织入-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>
    
    <!--配置事务切入-->
    <aop:config>
        <aop:pointcut id="txPointCut" expression="execution(...)"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
    </aop:config>
    

    1、定义数据源

    2、定义事务管理器

    3、定义事务增强

    4、使用AOP对切面进行事务增强


  2. 注解式

    1. 定义数据源
    2. 定义事务管理器
    3. 开启事务注解驱动
    4. 在类或者方法上使用@Transaction注解

Spring依赖循环

什么是循环依赖?

两个或者两个以上的bean互相依赖对方 ,最终形成闭环

public class A{
	private B b;
}
public class B{
private A a;
}

解决循环依赖的前提

  1. 不全是构造器方式的循环依赖
  2. 必须是单例的,否则无法解决

须知:Spring创建对象时分布进行的,首先创建目标对象(实例化)然后为其注入属性(初始化)

三级缓存

private final Map<String,Object> signletonObjects = new ConcurrentHashMap<>(256);
private final Map<String,Object> earlySingletonObjects = new HashMap<>(16);
private final Map<String,ObjectFactory<?>> singletonFactories = new HashMap<>(16);
  • singletonObjects 第一级缓存,存放可用的成品bean
  • earlySingletonObjects 第二级缓存,存放半成品Bean 是已创建对象,但是未完成初始化,用以解决循环依赖
  • singletonFactories 第三级缓存,存的是Bean工厂对象

    用来生成半成品的Bean 并放入到二级缓存中

步骤

  • 首先创建A对象,为其注入属性时发现其依赖对象B,所以需要注入对象B
  • 发现缓存中没有对象B,此时将A对象放入半成品缓存,然后去创建B对象
  • B对象的创建需要依赖A对象,于是从半成品缓存里取出半成品A进行注入
  • B对象继续注入其他属性和初始化,之后将完成品B对象放入完成品缓存
  • A对象继续注入属性,从完成品缓存中取到完成品B对象并注入
  • A对象继续注入其他属性和初始化,之后将完成品A对象放入完成品缓存
Little Question

一定要使用三级缓存解决循环依赖吗?只用二级缓存可以吗?

​ 不可以,违背了Spring的设计原则

如果要使用二级缓存解决循环依赖,意味着Bean在构造完后就创建代理对象,这样违背了Spring设计原则.Spring结合AOP跟Bean的声明周期,是在Bean创建完全之后通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来完成的,在这个后置处理的postProcessAfterInitialization 方法中对初始化后的Bean完成AOP代理。如果出现了循环依赖,那没有办法,只有给Bean先创建代理,但是没有出现循环依赖的情况下,设计之初就是让Bean在生命周期的最后一步完成代理,而不是在实例化后就立马完成代理

三级缓存总结

  1. 属性注入可以破解
  2. 构造器不行 三级缓存没有自己,因二级之后去加载B了
  3. 三级缓存
    1. 去单例池拿
    2. 判断是否是正在被创建的
    3. 判断是否 支持循环依赖
    4. 二级缓存 放到 三级缓存
    5. 干掉二级缓存 GC
    6. 下次再来直接 三级缓存拿, 缓存
  4. 缓存 存放
    1. 一级缓存 单例Bean
    2. 二级缓存 工厂 产生Bean 产生Bean复杂
    3. 三级缓存 半成品

一些小问题以及坑

1、Spring中的Bean 是线程安全的吗?

  1. 原型(propotype)Bean
    1. 每次创建对象的时候,是线程安全的
  2. 单例Bean
    1. 看具体情况,如果是无状态的Bean就是线程安全的
    2. 存在成员变量时在多线程下会出现线程安全问题
    3. 如果非要使用共享变量的话,可以使用ThreadLocal

2、Spring框架中的Controller 是单例吗?

  1. Controller 默认是单例的,不保证线程安全,会导致其属性会被重复使用

  2. 如何解决??

    1. 不要在controller中定义成员变量

    2. 可以通过@Scope(”property”) 将其设置为多例模式

    3. 在controller层中使用ThreadLocal

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值