常规后端开发
开发规范-Restful
REST: Representational State Transfer, 表述性状态转换, 是一种软件架构风格
传统风格
http://localhost/user/getById?id=1 GET
http://localhost/user/saveUser POST
http://localhost/user/updateUser POST
http://localhost/user/deleteUser?id=1 GET
REST风格
http://localhost/users/1 GET: 查询id为1的用户
http://localhost/users POST: 新增用户
http://localhost/users PUT: 修改用户
http://localhost/users/1 DELETE: 删除id=1的用户
URL定位资源, HTTP动词描述操作
统一的响应结果Result
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Integer code;
private String msg;
private Object data;
// 成功响应
public static Result success() { return new Result(1, "success", null); }
// 查询类的成功
public static Result success() { return new Result(1, "success", data); }
// 失败响应
public static Result error() { return new Result(0, msg, null); }
}
Lombok有一个很有用的注解, 日志 @Slf4j
就可以使用 log.info("xxx");
具体的使用看提示
指定请求方式
@Slf4j
@RestControler
public class DeptController {
// @RequestMapping(value="/depts", method = RequestMethod.GET)
@GetMapping("/depts")
public Result list() {
log.info("check all data");
return Result.success();
}
@DeleteMapping("/depts/{id}")
public Result delete(@PathVariable Integer id) { // @PathVariable 表示路径参数
log.info("delete dept with id = {}", id);
return Result.success();
}
@PostMapping("/depts")
public Result save(@ReequestBody Dept dept) { // @ReequestBody 表示参数是json
return Result.success();
}
}
像上面这种情况, 会有一个问题, 同一个Controller中, 基本上会有一个基本相同的路径 /depts
, 一般的做法是
@Slf4j
@RestControler
@RequestMapping("/depts") // 添加共同的根路径
public class DeptController {
// @RequestMapping(value="/depts", method = RequestMethod.GET)
@GetMapping
public Result list() {
log.info("check all data");
return Result.success();
}
@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id) { // @PathVariable 表示路径参数
log.info("delete dept with id = {}", id);
return Result.success();
}
@PostMapping
public Result save(@ReequestBody Dept dept) { // @ReequestBody 表示参数是json
return Result.success();
}
}
分页查询
定义Mapping的时候可以设置默认值
// page 起始索引 pageSize 每页包含多少条数据
@GetMapping("/emps")
public Result page(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize) {
log.info("分页查询, {}, {}", page, pageSize);
return Result.success();
}
分页插件PageHelper
的使用
如果正常开发, 想要使用实现分页查询, 至少需要两条sql语句
select count(*) from emp;
selct * from emp limit 0, 5;
基本上所有的分页操作都是这么实现的, 那么可以使用插件来简化这个操作, 只需要保留一个查询所有的语句就行
引入插件
<dependency>
<groupId>com.github.pagehelper</grouId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
@Override
public PageBean page(Integer page, Integer pageSize) {
PageHelper.startPage(page,pageSize);
List<Emp> empList = empMapper.list();
Page<Emp> p = (Page<Emp>) empList;
PageBean pageBean = new PageBean(p.getTotal(), p.getResult());
return pageBean;
}
文件上传
// 前端 传递图片数据的设置
MIME type
application/x-www-form-urlencoded // default
multipart/form-data // 文件
text/plain // 普通文本
@PostMapping("/upload")
public Result upload(String uname, Integer age, MultipartFile image) {
// MultipartFile 专门用来接收二进制数据, 默认上传只能1M
// image 要跟参数名保持一致
// getSize(); 文件大小, 字节
// getBytes(); 文件的内容的字节数组
// getInputStream(); 文件内容输入流
// 需要构造一个唯一文件名
String originalFilename = image.getOriginalFilenaeme();
int index = originalFilename.lastIndexOf(".");
String extname = originalFilename.substring(index);
String newFileName = UUID.randomUUID().toString() + extname;
// 保存到服务器本地
image.transferTo(new File("/xx/xx/" + newFileName));
// 上传到阿里云 直接封装到工具类中
aliOSSUtils.upload(image); // 这个工具类直接使用@Component注解
return Result.success();
}
修改上传大小限制application.properties
# 单个文件上传大小
spring.servlet.multipart.max-file-size=10MB
# 多个文件单个上传的大小
spring.servlet.multipart.max-request-size=100MB
上传到阿里云
对象存储OSS(Object Storage Service)
- 添加依赖, java9以上还需要添加别的依赖
<dependency>
<groupId>com.aliyun.oss</grouId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.1</version>
</dependency>
- 上传文件
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessKeyId = "yourAccessKeyId";
String accessKeySecret = "yourAccessKeySecret";
// 填写Bucket名称,例如examplebucket。
String bucketName = "examplebucket";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = "exampledir/exampleobject.txt";
// 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。
String filePath= "D:\\localpath\\examplefile.txt";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, new File(filePath));
// 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
// ObjectMetadata metadata = new ObjectMetadata();
// metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
// metadata.setObjectAcl(CannedAccessControlList.Private);
// putObjectRequest.setMetadata(metadata);
// 设置该属性可以返回response。如果不设置,则返回的response为空。
putObjectRequest.setProcess("true");
// 上传文件。
PutObjectResult result = ossClient.putObject(putObjectRequest);
// 如果上传成功,则返回200。
System.out.println(result.getResponse().getStatusCode());
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
配置文件
application.properties配置
主要作用是集中管理参数配置
1. 在application.properties 中先定义参数并赋值
2. 在具体的类中使用@Value("${配置文件中的key}")注解来进行赋值
application.properties
aliyun.oss.endpoint=http://xxxx
aliyun.oss.accessKeyId=xxxx
@Component
public class AliOSSUtils {
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.accessKeyId}")
private String accessKeyId;
}
yml配置文件
配置方式稍有不同
XML
<server>
<port>8080</port>
<address>127.0.0.1</address>
</server>
application.properties
server.port=8080
server.address=127.0.0.1
application.yml / .yaml (推荐)
server:
port:8080
address:127.0.0.1
yml语法
- 大小写敏感
- 数值前必须有空格, 作为分隔符
- 使用缩进表示层级关系, 缩进`不允许使用tab`, 只能用空格
- 缩进空格数不重要, 只要相同层级元素左对齐就行
- # 表示注释
对象, map 集合
user:
name: zz
age: 18
pwd: 123
数组list
hobby:
- java
- game
- sport
Tips: 可以直接删除application.properties, 新建一个application.yml文件就可以了
@ConfigurationProperties注解
这个注解的作用, 主要是在引用配置的端口, 引用的时候是要使用`@Value`注解, 每个属性上都需要使用, 如果使用`@ConfigurationProperties`注解的话, 只需要一个注解, 然后保持配置文件和引用的属性保持一致
// 定义阿里云相关的配置类
@ConfiturationProperties(prefix='aliyun.oss')
public class AliOSSProperties {
private String ednpoint;
private String accessKeyId;
xxxx
xxxx
}
// 具体的工具类, 直接使用DI即可
@Component
public class AliOSSUtils {
@Autowired
private AliOSSProperties aliOSSProperties;
}
// 这里报错的话, 需要引入一个依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
拦截技术
会话技术
会话:
用户打开浏览器, 访问web服务器的资源, 会话建立, 在一次会话中可以包含多次请求和响应
会话跟踪:
服务器需要识别多次请求是否来自同一个浏览器, 以便在同一次会话的多次请求间共享数据
会话跟踪技术分三种
客户端: Cookie, Http 天生支持
// 设置响应cookie
public Result cookie1(HttpServletResponse res) {
res.addCookie(new Cookie("login_name", "kk"));
return Result.success();
}
// 获取浏览器cookie
public Result cookie2(HttpServletRequest req) {
Cookie[] cookies = req.getCookies();
for (Cookie cookie : cookies) {
xxxx
}
return Result.success();
}
客户端cookie的缺点
- 移动端App无法使用
- 不安全, 用户可以禁用cookie
- Cookie 不能跨域
服务端: Session
浏览器第一次请求的时候, 是没有session
, 这个时候服务端会自动创建一个Session添加到cookie中返回给客户端
// 设置session
public Result cookie1(HttpSession session) {
session.setAtribute(new Cookie("login_name", "kk"));
return Result.success();
}
// 获取session
public Result cookie2(HttpServletRequest req) {
HttpSession session = reques.getSession();
Object loginUser = session.getAttribute("login_name");
return Result.success();
}
优点: 存储在服务器, 安全 缺点: 服务器集群情况下无法使用, Cookie的缺点都有
JWT令牌(Json Web Token) jwt.io
定义了一种简洁的, 自包含格式, 用于在通信双方以json数据格式安全的传输信息, 因为数字签名的存在, 因此是安全的
base64: 是一种基于64个可打印字符(A-Z a-z 0-9 + /)来表示二进制数据的编码格式
JWT 是由三部分组成, 以 . 进行分割
第一部分:
Header, 记录令牌类型, 签名算法
第二部分:
Payload, 携带自定义信息, 默认信息
第三部分:
Signature, 防止Token被篡改, 确保安全性, 将header和payload加入指定秘钥, 通过指定签名算法计算而成
JWT令牌的生成
1. 添加依赖 pom.xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2. 生成
Map<String, Objec> claims = new HashMap<>();
claims.put("id", 1);
claims.put("username", "tom");
String jwt = Jwts.builder()
.setClaims(claims) // payload
.signWith(SignatureAlgorithm.HS256, "kkkk") // 签名算法
.setExpiration(new Date(System.currentTimeMillis() + 12 * 3600*1000)) // 有效期
.compact();
3. 解析
Jwts.parser()
.setSigningKey("kkkk")
.parseClaimsJws(jwt字符串)
.getBody()
Filter
JavaWeb的三大组件Servlet
: 处理请求, Filter
: 过滤器, Listener
: 监听器, 常用的只有过滤器了
过滤器一般完成一些通用操作, 比如登录校验, 统一编码处理, 敏感字符处理等
Filter快速入门
- 定义Filter类, 实现Filter接口, 并重写所有方法
// /login 指定拦截, /emp/* 指定目录下的所有, /* 拦截所有
@WebFilter(urlPatterns="/*") // 拦截所有url
public class CusFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletExecption { // web服务启动, 调用一次
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// 放行前逻辑
// 这个是放行, 允许访问相对应的资源
// 过滤器链, 可以存在多个过滤器, 每次访问, 所有的过滤器都会走一遍
// 执行顺序, 是按照过滤器类名的自然排序来排名的(字符串)
chain.doFilter(request, response);
// 放行后, 依然会执行到这里
}
@Override
public void destroy() { // 销毁的时候调用一次
Filter.super.destroy();
}
}
- 配置Filter, Filter类上加
@WebFilter
注解, 引导类上加上@ServletComponentScan
开启Servlet组件支持
引导类, 就是带有 @SpringBootApplication 的启动类
@ServletComponentScan // 表示支持servlet组件
@SpringBootApplication
public class xxx {
}
登录过滤
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
String url = req.getRequestURL().toString();
if(url.contains("login")) {
// 放行
chain.doFilter(request, response);
return;
}
// 获取token
String jwt = req.getHeader("token");
// 不存在, 没有登录
if(!StringUtils.hasLength(jwt)) {
Result error = Result.error("NOT_LOGIN");
// 转成json, 使用fastjson
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
// 存在, 解析失败
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
Result error = Result.error("NOT_LOGIN");
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(NOT_LOGIN);
}
// 没问题, 放行
chain.doFilter(request, response);
return;
// 放行后, 依然会执行到这里
}
Interceptor
类似过滤器, Spring框架提供的
快速入门
- 定义拦截器, 实现
HandlerInterceptor
接口, 并重写所有方法
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
// 处理前, 返回false 则不放心
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
// 登录校验逻辑
return true;
}
// 拦截后执行
public void posthandle(HttpServletRequest req, HttpServletResponse resp,Object handler, ModelAndView modelAndView) {
}
// 视图渲染完毕后执行
public void afterCompletion(HttpServletRequest req, HttpServletResponse resp, Object handler, Exception ex) {
}
}
- 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/**") // 匹配哪些路径 /* 一级路径, /** 任意路径, /depts/* depts下的一级路径, /depts/** depts下的任意路径
.excludePathPatterns("/login"); // 不需要过滤的路径
}
}
异常处理
一般的请求响应流程是:
-req-> Controller --> Service --> Mapper
-res->Mapper --> Service --> Controller
一旦产生异常, 最有效的做法是全局异常处理器, 不然就要每个请求都添加try - catch
@RestControllerAdvice // = @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {
// 表示捕获所有异常
@ExceptionHandler(Exception.class)
public Result ex(Exception ex) {
ex.printStackTrace();
return Result.error("操作失误, 请联系管理员");
}
}
事务 @Transactional
@Transactional:
可以放到 业务的方法上, 类上, 或者接口
将当前的方法提交给spring进行事务管理, 一般是在需要执行多次sql操作的业务方法上添加注解即可
// 删除部门, 那么同时要把部门下的人也干掉
@Transactional
public void delete(Integer id) {
// 删除部门
deptMapper.deleteById(id);
// 删除部门下的员工
empMapper.deleteByDeptId(id);
}
开启Spring事务管理日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionmanager:debug
rollbackFor
正常情况下, 只有出现`RuntimeException`才会回滚, `rollbackFor`属性用于控制出现任何异常类型, 都会回滚
`@Transactional(rollbackFor = Exception.class)`
propagation
事务的传播行为, 当一个事务方法被另一个事务方法调用时, 这个事务方法应该如何进行事务控制
a 和 b都开启了事务, a 内部调用了b
@Transactional
public void a() {
userService.b();
}
@Transactional(propagation = Propagation.REQUIRED)
public void b() {
xxx
}
REQUIRED 默认值, 需要事务, 有则加入, 加入到当前的事务, 无则创建新事务
REQUIRES_NEW 需要新事务, 总是创建新事务
SUPPORTS 支持事务, 有则加入, 无则在无事务状态中运行
NOT_SUPPORTED 不支持事务, 在无事务状态下运行, 如果存在已有事务, 则挂起当前事务
MANDATORY 必须有事务, 否则抛异常
NEVER 必须没有事务, 否则抛异常
例子:
删除部门, 无论成功与否要记录到日志
try {
删除部门
} finally {
// 记录到日志
insert();
}
这么写之后, 就会出现问题, 删除部门的操作会和下面的insert共用一个事务, 如果删除部门出错, 就会导致添加日志的也会回滚掉,所以, insert()方法上要添加注解
@Transactional(propagation = Propagation.REQUIRES_NEW) // 开启新事务
public void insert() { }
控制台日志插件Grep Console
可以高亮显示
AOP Aspect Oriented Programming 面向切面编程, 面向特定方法编程
AOP基本使用
运行耗时计算, 埋点, 权限控制, 记录日志之类的操作, 上面的事务底层也是通过aop来实现的, 类似动态代理技术
统计各个业务层方法执行耗时
- 导入AOP依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 定义模板方法
@Component
@Aspect
public class TimeAspect {
@Around("execution(* com.kk.service.*.*(..))") // 指定哪些方法需要 报名下 所有的类所有的方法
public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) {
long begin = System.currentTimeMillis();
Object object = proceedingJoinPoint.proceed(); // 调用原始方法运行
long end = System.currentTimeMillis();
log.info(proceedingJoinPoint.getSignature() + " 执行耗时: {} ms", end - begin);
return object;
}
}
上面的方法可以使用另一个注解简化@Pointcut
抽取出公共的切入点
@Pointcut("execution(* com.kk.service.impl.DeptServiceImpl.*(..))")
public void pt() {} // 这里的public 都能引用 private 只能在这个类里
@Component
@Aspect
public class TimeAspect {
@Around("pt()") // 指定哪些方法需要 报名下 所有的类所有的方法
public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) {
long begin = System.currentTimeMillis();
Object object = proceedingJoinPoint.proceed(); // 调用原始方法运行
long end = System.currentTimeMillis();
log.info(proceedingJoinPoint.getSignature() + " 执行耗时: {} ms", end - begin);
return object;
}
}
连接点 JointPoint:
可以被Aop控制的方法, 也就是业务方法
通知 Advice:
指重复的逻辑, 共性功能, 比如上面那个 recordTime
切入点 PointCut:
匹配连接点的条件, 通知仅会在切入点方法执行时被应用 @Around指定的
切面 Aspect:
描述通知与切入点的对应关系, 也就是以上三个放到一块被称为切面, 或者切面类 TimeAspect
目标对象 Target:
通知所应用的对象, 要对哪个类生效
AOP的执行流程
, 在执行的时候, 就相当于生成了一个代理对象, 就是把要还行的代码放到上面recordTime这个方法里, 调用业务代码的时候, 就相当于是执行了代理类组成的方法, 业务层上面是无法感知的
AOP进阶
通知类型
@Around:
环绕通知, 在链接点前后都执行
@Before:
前置通知, 连接点方法之前
@After:
后置通知, 连接点之后执行, 无论是否有异常都会执行
@AfterReturning:
返回后通知, 有异常不会执行
@AfterThrowing:
有异常后执行
通知顺序
多个切面类, 针对一个方法, 执行的顺序是, 类名的字母排序有关系, 谁靠前就先执行谁, 还可以通过Order(1)
注解
切入点表达式
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
带 ?
表示是可以省略
* :
单个的任意一个 execution(* com.*.service.*.update*(*)
第一个*: 任意权限
第二个*: 第二级包是任意的
第三个*: 类或者是接口是任意的
第四个*: 以update开头后面是任意的字符的方法/接口
第五个*: 任意类型的一个参数
如果是*service, 表示以service结尾的
.. :
多个连续的任意符号execution(* com..DeptService.*(..)
第一个 .. : 表示任意的包名
第二个 .. : 表示任意的参数
Pointcut 链接多个:
Pointcut("execution(* com..DeptService.(..) || " + "execution( com..service..update*(*)"), ! || && 都可以使用
@annotation 表达式:
匹配表示有特定注解的方法
-
直接创建一个注解类 Annotaion
@Retention(RetentionPolicy.RUNTIME) // 运行时生效 @Target(ElementType.METHOD) // 在方法上生效 public @interface MyLog { }
-
在需要使用的方法上添加注解
@MyLog
@Override
public List<Dept> list() {
xxxx
}
- 切面类的修改
@Component
@Aspect
public class Aspect1 {
@Pointcut("@annotaion(com.kk.aop.MyLog)") // 凡是有MyLog注解的, 需要生效
privatte void pt() { }
@Before("pt()")
public void before() {
}
}
连接点
可以获取方法执行时的相关信息, 目标类名, 方法名, 参数等...
1. 对于@Around , 获取连接点只能使用ProceedingJoinPoint
2. 其他四种可以直接使用JointPoint, 这个是上面的父类型
jointPoint.getTarget().getClass().getName(); // 获取目标类名
jointPoint.getSignature(); // 获取目标方法签名
jointPoint.getSignature().getName(); // 获取目标方法名
jointPoint.getTarget().getArgs(); // 获取目标方法参数
jointPoint.proceed(); // 获取方法本身
Bean对象相关
获取Bean对象
// 主要就是这个类 ApplicationContext
@Autowired
private ApplicationContext applicationContext; // IOC容器对象
application.getBean("deptController"); // 根据名称
application.getBean(DeptController.class); // 根据类型
application.getBean("deptController", DeptController.class); // 根据名称和类型
Bean对象作用域
Spring 支持五中作用域, 后三种在web才生效
singleton:
容器内同名称的bean只有一个实例, 单例, 默认设置
prototype:
每次使用该bean会创建一个新对象
request:
每个请求
session:
么个session
application:
每一个应用范围
通过@Scope
注解来进行配置作用域
@Lazy // 延迟, 等到使用的时候才会实例化
@Scope("prototype")
@RestController
public class DeptController {
}
第三方Bean
如果要管理第三方, 比如引用的第三方库, 是无法使用@Component
及衍生注解声明bean的, 需要使用@Bean
注解
一般是搞一个配置类, 集中管理第三方
@Configuration
public class CommonConfig {
@Bean // 将返回值交给 IOC容器管理
public TestReader reader() {
return new TestReader();
}
}
Tips:
这里需要注意, 一般情况下, 加入到IOC容器管理的名称默认是首字母小写, 也可以通过name或者value指定, 第三方比较特殊, 不是类名的首字母小写, 而是方法名比如上面的是reader
而不是testReader
如果想要在第三方注入自己写的对象, 直接传参即可
@Configuration
public class CommonConfig {
@Bean // 将返回值交给 IOC容器管理
public TestReader reader(DeptController contro) {
return new TestReader();
}
}
Springboot 简单原理
Spring:
java开发框架, 集成了很多优秀的框架, 但是比较麻烦 依赖, 配置
Spring Boot:
Spring4.0之后提出来的, 目的是简化Spring, 能够让开发者快速开发部署(起步依赖, 自动配置)
- 起步依赖: 包管理, 安卓的gradle, iOS的cocoapods
- 类和配置,个人理解就是IOC和DI相关
自动配置的起点@SpringBootApplication
开始, 源码之中有三个比较重要的注解
@SpringBootConfiguration: 用来声明当前类也是一个配置类 @Configuration相同作用
@ComponentScan: 组件扫描, 默认扫描当前引导类所在包及其子包, 就是在这配置的
@EnableAutoConfigation: 实现自动配置的核心注解 会导入👇🏻这两个文件, 该jar包中所包含的类(全类名)
META-INF/spring.factories // 低版本导入
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
通过这些导入到Ioc容器, 每个插件都有这两个文件
并不是所有在👆🏻两个文件出现的类都会被注入到Ioc容器, 因为类里面都有很多条件注解@Conditional
自动配置中有一个常用的注解@Conditional
条件注解
@ConditionalOnClass:
有对应的字节码文件, 才会注册bean到Ioc容器
@ConditionalOnMissingBean:
判断没有对应的bean, 才会注册到Ioc
@ConditionalOnProperty:
判断环境中是否有对应的属性和值, 才会注册到Ioc
转载自:https://juejin.cn/post/7248453787215396925