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常用注解
文章目录
- 文章目录
- 0.前言
- 1.
@SpringBootApplication
- 2. Spring Bean 相关
- 3. 处理常见的 HTTP 请求类型
- 4. 前后端传值
- 5. 读取配置信息
- 6. 参数校验
- 7. 全局处理 Controller 层异常
- 8. JPA 相关
- 9. 事务
@Transactional
- 10. json 数据处理
- 11. 测试相关
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 层异常。
相关注解:
@ControllerAdvice
:注解定义全局异常处理类@ExceptionHandler
:注解声明异常处理方法
如何使用呢?拿我们在第 5 节参数校验这块来举例子。如果方法参数不对的话就会抛出MethodArgumentNotValidException
,我们来处理这个异常。
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
/**
* 请求参数异常处理
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, HttpServletRequest request) {
......
}
}
更多关于 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);
}
}
简单介绍一下上面设计到的一些注解:
-
@CreatedDate
: 表示该字段为创建时间时间字段,在这个实体被 insert 的时候,会设置值 -
@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 事务的内容请查看:
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作用域
-
singleton
-
singleton :单例模式 (多线程下不安全)
-
singleton:单例模式,Spring IoC 容器中只会存在一个共享的 Bean 实例,无论有多少个
Bean 引用它,始终指向同一对象。该模式在多线程下是不安全的。Singleton 作用域是
Spring 中的缺省作用域,也可以显示的将 Bean 定义为 singleton 模式,配置为:
-
-
-
prototype
-
prototype: 原型模式 每次使用时创建
-
prototype:原型模式,每次通过 Spring 容器获取 prototype 定义的 bean 时,容器都将创建
一个新的 Bean 实例,每个 Bean 实例都有自己的属性和状态,而 singleton 全局只有一个对
象。根据经验,对有状态的bean使用prototype作用域,而对无状态的bean使用singleton
作用域。
-
-
-
request(Web环境特有)
-
Request :一次 request 一个实例
-
request:在一次 Http 请求中,容器会返回该 Bean 的同一实例。而对不同的 Http 请求则会
产生新的 Bean,而且该 bean 仅在当前 Http Request 内有效,当前 Http 请求结束,该 bean
实例也将会被销毁。
-
-
-
session(Web环境特有)
-
session:在一次 Http Session 中,容器会返回该 Bean 的同一实例。而对不同的 Session 请
求则会创建新的实例,该 bean 实例仅在当前 Session 内有效。同 Http 请求相同,每一次
session 请求创建新的实例,而不同的实例之间不共享属性,且实例仅在自己的 session 请求
内有效,请求结束,则实例将被销毁。
-
-
global session(Web环境特有)
-
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
-
BeanFactory是 Spring框架的基础设施,面向Spring本身;
ApplicationContext面向使用Spring框架的开发者,几乎所有的应用场合我们都直接使用ApplicationContext而非底层的BeanFactory。
-
BeanFactory顶层接口
- 位于类结构树的顶端,它主要的方法就是getBean(String beanName) 该方法从容器中返回特定名称的Bean BeanFactory的功能通过其他的接口得到不断扩展
-
SingletonBeanResitry 运行期间注册单例Bean
定义了允许期间向容器注册单例Bean的方法;对于单实例(singleton)的Bean来说,BeanFactory会缓存Bean实例,所以第二次使用getBean获取Bean时将直接从IoC容器的缓存中获取Bean实例,Spring在DefaultSingletonBeanResitory类中提供一个用于缓存单实例Bean的缓存器,它是一个用HashMap实现的缓存器
-
依赖日志管理插件
在初始化BeanFactory时,必须为其提供一种日志框架,比如使用Log4j,slf4j,这样在启动时才不会报错
-
顶层接口BeanFactory和ApplicationContext的区别
-
- 单例对象适用
- ApplicationContext在构建核心容器时,创建对象采用的是立即加载的方式,即配置文件一读取完,立马创建对象
- 多例对象适用
- BeanFactory在构建核心对象时采用的是延迟加载的方式,即什么时候通过Id获取对象,什么时候创建
- 单例对象适用
ApplicationContext
ApplicationContext常用实现类
- ClassPathXmlApplicationContext 读取类路径下的配置文件
- FileSystemXmlApplicationContext 读取电脑任意位置的配置文件
- AnnotationConfigApplicationContext 用于新注解+配置类的方式替代xml配置文件时
WebApplication体系架构
是专门为web应用准备的,它允许从相对Web根目录的路径中装载配置文件完成初始化工作。从WebApplicationContext中可以获得ServletContext的引用,整个Web应用上下文对象将作为属性放置到ServletContext中,以便Web应用环境可以访问Spring上下文应用。
Spring 依赖注入 DI 四种方式
构造器注入
- 必须要有构造器
- bean标签下标签
- name:属性名
- value:值,针对基本类型和String
- ref:针对对象类型
/*带参数,方便利用构造器进行注入*/
public CatDaoImpl(String message){
this.message=message;
}
<bean id="CatDaoImpl" class="co m.CatDaoImpl">
<constructor-arg value="message"></constructor-arg>
</bean>
setter方法
- 条件:属性必须要有setter方法
- bean标签下标签
- name:属性名
- value:属性值,针对基本属性和String类型
- 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>
}
静态工厂注入
- 静态工厂:顾名思义,就是通过调用静态工厂的方法来获取自己需要的对象,为了让spring管理所有对象,我们不能直接通过**工程类.静态方法()**来获取对象,而是依然通过spring注入的方式获取
- 条件:需要工厂类,该工厂类中需要有静态方法
- 配置文件语法 <bean id = factory class =“com.zzub.factory.BeanFactory" factory-method=“静态方法名”/>
- 当用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 工厂类,再调用普通的
- 条件:需要工厂类,这个工厂类中需要有普通方法
- 配置文件语法
- 需要先创建工厂对象再调用工厂中的普通方法。
实例方法
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 开发时较常用的方式
全自动配置方式
- 基本不用配置文件
- 在类上使用
@Component
注解标记 类中的依赖关系使用@Autowired
注解标记 - 使用@MapperScan注解
- **注意,如果一个类型匹配到了多个实现类,那么必须去使用
@Qualifier
注解指明需要的类 - 四个创建对象的注解
- @Component 无法划分类的时候
- @Repository 一般用户标注Dao(Mapper)层 或者说标注数据库操作层
- @Service 业务层
- @Controller 控制层/表现层
- 三个依赖注入时的注解
- @Value 针对基本数据类型和String类型
- @Autowired
- 该注解由Spring框架提供
- 按类型去寻找,如果找不到那就是没有,如果找到多个就报错,解决方式就是搭配@Qualifier注解指明使用哪一个注解
- @Resource
- 该注解由JDK提供
- 先按照名字去找,第一种是name属性配置名字@Resource(name=“名字”) ,如果没有指定名字,则把变量名当作要寻找的属性名,如果再找不到,最后按照类型去找。
AOP
面向切面
-
AOP叫做面向切面编程,目的是在不修改原有代码的基础上,把新的功能植入到程序中,提高代码的模块性
-
SpringAOP基于动态代理的方式实现,如果是接口的话就会使用JDK动态代理,反之使用CGLIB
- JDK动态代理: 实现接口,java反射机制生成一个代理接口的匿名类 ,调用具体方法的时候调用invokeHandler
- CGLIB 基于ClassLoad装载,修改字节码生成子类去处理
好处解耦!
-
底层原理-动态代理
静态代理与动态代理的区别?
- 静态代理每次增强的时候都会创建一个新类,代码看起来十分臃肿
- 静态代理在代理前就知道要代理的对象,动态代理运行时才直到
- 静态代理一般只能代理一个类,动态代理实现了接口的多个子类
JDK动态代理和CGLIB 动态代理的区别
- JDK动态代理要求被代理对象必须实现接口,基于反射的机制实现,生成一个实现同样接口的一个代理类,然后通过重写方法的方式实现,实现对代码的增强
- 如果被代理类没有实现接口则使用CGLIB动态代理,基于ASM 第三方框架,通过修改字节码生成一个子类,然后重写父类的方法,实现对代码的增强
- 1.8之前CGLIB性能高一些,但1.8之后性能差不多
适用场景
- 性能检测
- 访问控制
- 事务管理
- 日志记录
- 权限控制
- 分布式追踪
- 缓存控制
术语
连接点(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实现)
-
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对切面进行事务增强
-
注解式
- 定义数据源
- 定义事务管理器
- 开启事务注解驱动
- 在类或者方法上使用@Transaction注解
Spring依赖循环
什么是循环依赖?
两个或者两个以上的bean互相依赖对方 ,最终形成闭环
public class A{ private B b; } public class B{ private A a; }
解决循环依赖的前提
- 不全是构造器方式的循环依赖
- 必须是单例的,否则无法解决
须知: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在生命周期的最后一步完成代理,而不是在实例化后就立马完成代理
三级缓存总结
- 属性注入可以破解
- 构造器不行 三级缓存没有自己,因二级之后去加载B了
- 三级缓存
- 去单例池拿
- 判断是否是正在被创建的
- 判断是否 支持循环依赖
- 二级缓存 放到 三级缓存
- 干掉二级缓存 GC
- 下次再来直接 三级缓存拿, 缓存
- 缓存 存放
- 一级缓存 单例Bean
- 二级缓存 工厂 产生Bean 产生Bean复杂
- 三级缓存 半成品
一些小问题以及坑
1、Spring中的Bean 是线程安全的吗?
- 原型(propotype)Bean
- 每次创建对象的时候,是线程安全的
- 单例Bean
- 看具体情况,如果是无状态的Bean就是线程安全的
- 存在成员变量时在多线程下会出现线程安全问题
- 如果非要使用共享变量的话,可以使用
ThreadLocal
2、Spring框架中的Controller 是单例吗?
-
Controller 默认是单例的,不保证线程安全,会导致其属性会被重复使用
-
如何解决??
-
不要在controller中定义成员变量
-
可以通过@Scope(”property”) 将其设置为多例模式
-
在controller层中使用ThreadLocal
-