在说断点续传之前，先说一下文件上传的原理，因为断点续传的应用场景就是文件上传。文件上传客户端发送上传文件请求服务器收到请求之后解析请求头和请求体，获取上传的数据并进行验证（已经上传的文件不重复上传）服务器端将分片上传的文件存在临时目录中，所有分片上传后再进行合并和重命名最后将文件存储在指定的目录断点续传文件传输过程中，如果出现传输中断或者失败的情况，可以从中断的地方继续传输，不用重新开始传输。断点续传有很多种实现方式，这里推荐b站一个用Java实现多线程断点续传的视频<4.java多线程-实现断点续传_哔哩哔哩_bilibili>在实际应用时我选择用Redis实现断点续传大致的思路：前端计算上传文件的MD5值，如果Redis中由对应的MD5值则说明秒传成功后端通过Redis记录文件分片的情况（Redis中使用Set记录哪些分片已经上传，已经上传过的分片会被跳过），前端开始上传之后，每次上传成功都要记录在Redis中，续传之后前端重新计算MD5值，重新开始上传Redis中记录缺失的分片。当所有分片都上传完成并且合并成文件之后，删除Redis中对应的分片记录。以上思路只适用于单线程，如果要使用多线程方式实现，需要考虑更多的问题~代码实现 @Transactional(rollbackFor = Exception.class) public boolean uploadFile(MultipartFile file, String fileName, String fileMD5, String fileId, HttpServletRequest request, String redisKey) { try { // 参数校验 if (file == null || file.isEmpty()) { throw new IllegalArgumentException("文件不能为空"); } // 如果fileId为空，则生成一个随机的fileId if (StringUtils.isEmpty(fileId)) { fileId = RandomStringUtils.random(10); } // 从请求头中获取分片信息 int chunkNumber = 0; int totalChunks = 1; long chunkSize = file.getSize(); long totalSize = file.getSize(); String chunkMD5 = null; try { chunkNumber = Integer.parseInt(request.getHeader("Chunk-Number")); totalChunks = Integer.parseInt(request.getHeader("Total-Chunks")); chunkSize = Long.parseLong(request.getHeader("Chunk-Size")); totalSize = Long.parseLong(request.getHeader("Total-Size")); chunkMD5 = request.getHeader("Chunk-MD5"); } catch (Exception e) { log.info("未获取到分片信息"); } // 创建临时目录 String tempDir = System.getProperty("java.io.tmpdir") + "/upload/" + fileMD5 + "/"; File tempFileDir = new File(tempDir); if (!tempFileDir.exists()) { tempFileDir.mkdirs(); } // 秒传检查 File finalFile = new File("/final/path/" + fileId + "_" + fileName); if (finalFile.exists() && finalFile.length() == totalSize) { log.info("文件已存在，秒传成功"); return true; } // 处理分片上传 File chunkFile = new File(tempDir + "chunk_" + chunkNumber); // 断点续传检查 - 结合Redis Boolean chunkExistsInRedis = redisTemplate.opsForSet().isMember( String.format(CHUNK_UPLOAD_KEY, fileMD5), String.valueOf(chunkNumber)); if (chunkFile.exists() && chunkFile.length() == chunkSize && Boolean.TRUE.equals(chunkExistsInRedis)) { log.info("分片 {} 已存在，跳过上传", chunkNumber); } else { // 保存分片文件 File tempChunkFile = new File(tempDir + "chunk_" + chunkNumber + ".tmp"); file.transferTo(tempChunkFile); // 分片MD5校验 if (chunkMD5 != null && !chunkMD5.isEmpty()) { try (FileInputStream fis = new FileInputStream(tempChunkFile)) { MessageDigest digest = MessageDigest.getInstance("MD5"); byte[] buffer = new byte[8192]; int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { digest.update(buffer, 0, bytesRead); } byte[] md5Bytes = digest.digest(); StringBuilder sb = new StringBuilder(); for (byte b : md5Bytes) { sb.append(String.format("%02x", b)); } String actualChunkMD5 = sb.toString(); if (!chunkMD5.equals(actualChunkMD5)) { tempChunkFile.delete(); throw new RuntimeException("分片MD5校验失败"); } } } // 重命名临时文件 if (!tempChunkFile.renameTo(chunkFile)) { // 重命名失败时复制文件 try (FileInputStream fis = new FileInputStream(tempChunkFile); FileOutputStream fos = new FileOutputStream(chunkFile)) { byte[] buffer = new byte[8192]; int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { fos.write(buffer, 0, bytesRead); } } tempChunkFile.delete(); } // 将已上传的分片信息记录到Redis redisTemplate.opsForSet().add( String.format(CHUNK_UPLOAD_KEY, fileMD5), String.valueOf(chunkNumber)); // 设置过期时间（例如24小时） redisTemplate.expire( String.format(CHUNK_UPLOAD_KEY, fileMD5), 24, TimeUnit.HOURS); } // 检查是否所有分片都已上传完成 - 使用Redis记录的信息 long uploadedChunksCount = redisTemplate.opsForSet().size( String.format(CHUNK_UPLOAD_KEY, fileMD5)); boolean allChunksUploaded = uploadedChunksCount == totalChunks; // 如果所有分片都已上传，合并文件 if (allChunksUploaded) { // 创建合并后的文件 File mergedFile = new File(tempDir + "merged_" + fileMD5 + "_" + fileName); try (FileOutputStream fos = new FileOutputStream(mergedFile, true)) { for (int i = 0; i < totalChunks; i++) { File currentChunk = new File(tempDir + "chunk_" + i); try (FileInputStream fis = new FileInputStream(currentChunk)) { byte[] buffer = new byte[1024 * 8]; int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { fos.write(buffer, 0, bytesRead); } } currentChunk.delete(); } } // 验证合并文件的MD5 try (FileInputStream fis = new FileInputStream(mergedFile)) { MessageDigest digest = MessageDigest.getInstance("MD5"); byte[] buffer = new byte[8192]; int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { digest.update(buffer, 0, bytesRead); } byte[] md5Bytes = digest.digest(); StringBuilder sb = new StringBuilder(); for (byte b : md5Bytes) { sb.append(String.format("%02x", b)); } String actualFileMD5 = sb.toString(); if (!fileMD5.equals(actualFileMD5)) { throw new RuntimeException("文件MD5校验失败"); } } // 验证文件大小 if (mergedFile.length() != totalSize) { throw new RuntimeException("文件大小不匹配"); } // 保存到最终位置 File finalDir = new File("/final/path/"); if (!finalDir.exists()) { finalDir.mkdirs(); } if (finalFile.exists()) { finalFile.delete(); } Files.move(mergedFile.toPath(), finalFile.toPath(), StandardCopyOption.REPLACE_EXISTING); // 清理Redis中的分片记录 redisTemplate.delete(String.format(CHUNK_UPLOAD_KEY, fileMD5)); // 清理临时目录 File[] tempFiles = tempFileDir.listFiles(); if (tempFiles != null) { for (File tempFile : tempFiles) { tempFile.delete(); } } tempFileDir.delete(); // TODO 更新数据库记录 log.info("文件上传完成: {}", fileName); return true; } return true; } catch (Exception e) { log.error("文件上传失败", e); return false; } }
最近写题目的时候发现一些排序算法我不太熟悉，简单记录一下。下面所有的算法都会把数组元素按照升序排序。插入排序插入排序的核心思想是：遍历未排序的序列，对未排序序列的元素插入到已排序的序列中。插入排序的步骤：从第一个元素开始，该元素可以认为已经被排序。取出下一个元素，在已经排序的元素序列中从后向前扫描。如果该元素（已排序）大于新元素，将该元素移到下一位置。重复步骤3，直到找到已排序的元素小于或者等于新元素的位置。将新元素插入到该位置后。重复步骤2~5。public void insertionSort(int[] arr) { if (arr == null || arr.length <= 1) { return; // 数组为空或只有一个元素时无需排序 } for (int i = 1; i < arr.length; i++) { int key = arr[i]; // key是当前需要插入的元素 int j = i - 1; // 将比key大的元素向后移动 while (j >= 0 && arr[j] > key) { arr[j + 1] = arr[j]; // 元素后移 j--; } arr[j + 1] = key; // 把key插入到正确的位置 } }快速排序快速排序使用了“分治”的思想，每次选择一个元素，以这个元素为基准，将数组分为小于等于基准和大于基准的部分。再递归地对两边进行排序（也就是对左右两个区间再此选择基准进行划分，如此重复）。快速排序的步骤：选择基准：从数组中选择一个元素作为基准。基准元素的选择可以是随意的。分区：重新排列数组，使得所有小于基准的元素放在基准前面，大于基准的放在后面。分区结束后，基准处于数组的中间位置。递归：递归地将小于基准的子数组和大于基准的子数组排序。public void quickSort(int[] array) { if (array == null || array.length <= 1) return; sort(array, 0, array.length - 1); } private static void sort(int[] array, int low, int high) { if (low >= high) return; int pivot = array[high]; // 选择最后一个元素作为基准 int i = low - 1; // 较小元素的边界指针 // 分区操作：将小于基准的元素移到左侧 for (int j = low; j < high; j++) { if (array[j] <= pivot) { i++; swap(array, i, j); } } swap(array, i + 1, high); int pivotIndex = i + 1; // 递归排序两边子数组 sort(array, low, pivotIndex - 1); sort(array, pivotIndex + 1, high); } // 交换两个元素的方法 private static void swap(int[] array, int i, int j) { int temp = array[i]; array[i] = array[j]; array[j] = temp; }归并排序归并排序也用到了“分治”的思想，先将序列分为若干个有序的小区间，最后将这些区间逐步合并。归并排序的步骤：分解：将数组从中间分成两部分。解决：递归地对左右两部分进行归并排序。合并：将两个有序的子数组合并成一个有序数组。 public void mergeSort(int[] array) { if (array == null || array.length <= 1) return; int n = array.length; int[] temp = new int[n]; for (int size = 1; size < n; size *= 2) { // 内层循环处理每个子数组 for (int left = 0; left < n; left += 2 * size) { int mid = Math.min(left + size - 1, n - 1); int right = Math.min(left + 2 * size - 1, n - 1); // 合并操作 int i = left; int j = mid + 1; int k = left; // 比较并合并两个子数组 while (i <= mid && j <= right) { if (array[i] <= array[j]) { temp[k++] = array[i++]; } else { temp[k++] = array[j++]; } } // 处理剩余元素 while (i <= mid) temp[k++] = array[i++]; while (j <= right) temp[k++] = array[j++]; } } }堆排序堆排序的步骤：构建初始堆（这里以升序排序为例，构建大顶堆）。将堆顶元素与末尾元素交换，将最大元素"沉"到数组末端。重新调整堆结构（使其满足大顶堆性质），然后继续交换堆顶元素与当前末尾元素。反复执行调整+交换步骤，直到整个序列有序。public static void sort(int[] arr) { if (arr == null || arr.length <= 1) return; // 1. 构建大顶堆（从最后一个非叶子节点开始） for (int i = arr.length / 2 - 1; i >= 0; i--) { // 堆调整逻辑（原heapify方法内容） for (int root = i; root < arr.length; ) { int largest = root; int left = 2 * root + 1; int right = 2 * root + 2; // 找出根节点和子节点中的最大值 if (left < arr.length && arr[left] > arr[largest]) largest = left; if (right < arr.length && arr[right] > arr[largest]) largest = right; // 如果最大值不是当前根节点，交换并继续调整 if (largest != root) { int temp = arr[root]; arr[root] = arr[largest]; arr[largest] = temp; root = largest; // 移动到交换后的位置继续调整 } else { break; // 当前子树已满足堆性质 } } } // 2. 排序：逐个提取最大值 for (int size = arr.length - 1; size > 0; size--) { // 将堆顶元素（最大值）与当前末尾元素交换 int temp = arr[0]; arr[0] = arr[size]; arr[size] = temp; // 调整剩余元素 for (int root = 0; root < size; ) { int largest = root; int left = 2 * root + 1; int right = 2 * root + 2; // 找出根节点和子节点中的最大值 if (left < size && arr[left] > arr[largest]) largest = left; if (right < size && arr[right] > arr[largest]) largest = right; // 如果最大值不是当前根节点，交换并继续调整 if (largest != root) { int swapTemp = arr[root]; arr[root] = arr[largest]; arr[largest] = swapTemp; root = largest; // 移动到交换后的位置继续调整 } else { break; } } }桶排序这个算法在写题目的时候遇到了，我之前对这个算法不太熟悉，所以重新学一下。桶排序的思想是，找出序列中的最大值和最小值，然后分配若干个桶（桶的数量约等于序列中的元素个数），将每个元素放置到对应的桶中，对每一个桶中的元素进行排序，最后把所有桶中的元素整合成排序后的序列。步骤如下找到数组中的最大值和最小值，以确定数据的范围。确定桶的数量，每个桶代表一个区间范围。遍历数组，将每个元素分配到对应的桶中。对每个桶内的元素进行排序（使用Java内置的排序即可）。按顺序将各个桶中的元素合并回原数组。第三步中，确定桶的位置，我的理解是“相对”位置，即当前元素相对于序列中最小元素“后移”（可能我描述的不是很恰当，但我觉得看代码就能理解了）。 public static void bucketSort(int[] arr) { if (arr.length == 0) return; // 1. 找出数组中的最大值和最小值 int min = arr[0]; int max = arr[0]; for (int num : arr) { if (num < min) min = num; if (num > max) max = num; } // 2. 计算桶的数量和范围 int bucketCount = arr.length; // 通常桶的数量等于元素数量 int range = max - min + 1; // 3. 初始化桶 ArrayList<ArrayList<Integer>> buckets = new ArrayList<>(bucketCount); for (int i = 0; i < bucketCount; i++) { buckets.add(new ArrayList<>()); } // 4. 将元素分配到各个桶中 for (int num : arr) { // 计算元素应该放入哪个桶 int bucketIndex = (int) ((long) (num - min) * bucketCount / range); // 处理最大值的情况，确保它落在最后一个桶中 if (bucketIndex == bucketCount) bucketIndex--; buckets.get(bucketIndex).add(num); } // 5. 对每个桶内部进行排序 int index = 0; for (ArrayList<Integer> bucket : buckets) { Collections.sort(bucket); for (int num : bucket) { arr[index++] = num; } } }
什么是AOP？AOP就是面向切面编程，与OOP(面向对象编程)一样，是一种编程思想。AOP的思想运用在编程中可以在不改动原有设计的基础上进行功能的增强。AOP中的主要概念通知：简单来说就是抽取出的共性功能。切面：描述通知与切入点的关系，通俗来说就是哪些方法要追加哪些功能。连接点：就是方法，因为共性功能是要追加到方法里面的，因此连接点就是方法。切点：要抽取共性功能的方法称为切点，切点是连接点的子集。织入：将要添加的共性功能添加到的目标对象中的过程。AOP的原理其实对于初学者来说，不需要太清楚AOP的实现原理，但我还是决定写一下。知道它底层实现的原理也许能在未来解决一些bug。代理模式要讲AOP的底层原理，首先要讲什么是“代理”。假设有这样一个场景：你想要买一双鞋，但是一般情况下你是从商店里面买到这双鞋，而不是从这双鞋生产的地方买的。商店就在这一过程中充当了“代理”的角色。在买鞋的时候，你是很难直接接触到工厂的，而商店，这个“代理”者，能帮我们从工厂那里拿到鞋子。那么在购买这双鞋的过程中，商店就充当了“代理”的角色。示例这里我们使用aspectj来实现AOP项目背景：在一个项目中，调用接口时打印日志首先梳理一下大致的思路导入坐标到pom.xml文件中一般来说添加以下这两个依赖即可 org.springframework.boot spring-boot-starter-aop test org.aspectj aspectjweaver 首先创建一个打印日志的注解MyLogpackage com.blog.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD)//运用在方法上 @Retention(RetentionPolicy.RUNTIME)//在运行时执行 public @interface MyLog { String title() default ""; }然后制作共性功能这里的共性功能就是打印日志，我们就简单写一下在aspect包下创建一个切面类@AfterReturning注解在方法返回后进入切面的通知方法中package com.blog.aspect; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import java.util.Date; @Aspect @Component public class LogAspect { @AfterReturning(pointcut = "@annotation(com.blog.annotation.MyLog)") public void log(JoinPoint joinPoint) { System.out.println("log:"); handler(joinPoint); } protected void handler(JoinPoint joinPoint) { Date date = new Date(); String className = joinPoint.getTarget().getClass().getName(); System.out.println(className); System.out.println(date.getTime()); } } 这里面的pointcut指定为添加了MyLog注解的方法，也就是添加了@MyLog注解的方法才会进入AOP的逻辑中打印日志。
今天在Spring Boot与MySQL数据库进行连接的时候出现了以下报错(直接眼前一黑😢)，在网上进行搜索之后发现是maven依赖中的MyBatis与Spring Boot的版本不匹配解决方法如下：将MyBatis依赖的version改为2.2.2版本（与SpringBoot不发生冲突的版本）即可小小的感悟：出现爆红时先不要慌，记得复制报错信息搜索一下，说不定就是个很简单的BUG。以及maven依赖的报错真是数不胜数，简直是噩梦。 org.mybatis.spring.boot mybatis-spring-boot-starter 2.2.2 图片报错信息如下
最近在学一些后端的技术，零零散散记了一些相关的知识点，有关Spring的内容基本就集中在这一篇。看的教程网址：教程链接Spring核心概念IoC(Inversion of Control)控制反转使用对象时，由主动的new来产生对象转变为外部提供对象，此过程中对象创建控制权由程序转移到外部Spring提供一个容器IoC容器来充当“外部"这个容器复杂对象的创建、初始化等一系列工作，被创建或被管理的对象在IoC容器中统称为Bean目标：充分解耦使用IoC管理Bean(IoC)在IoC容器中将有依赖关系的bean进行关系绑定(DI)IoC思路分析IoC管理Service和Dao用配置将被管理的对象告知IoC容器用接口获取IoC容器用接口方法从容器中获取Bean1）导入spring坐标2）定义Spring管理的类(接口)3）创建Spring配置文件，配置对应的类作为Spring管理的Bean 注意：bean定义时id属性在同一个上下文中不能重复DI入门案例依赖注入，全称 Dependency Injection，简称 DI依赖关系的维护就称为依赖注入基于IoC管理BeanService中使用new形式创建的对象不能保留Service中需要的Dao对象通过方法进入Service中1）删除使用new形式创建对象的代码2）提供依赖对象对应的set方法3）配置service和dao之间的关系 BeanBean基础配置标签id:bean的id，使用容器可以通过id值获取对应的bean，在一个容器中id值唯一class:bean的类型，即配置的bean的店路径名name:bean的别名，可以使用空格分隔多个别名scope:作用范围，可以选择单例或者多例，默认单例适合交给容器进行管理的Bean表现层对象业务层对象数据层对象工具对象Controller:控制层，接受前端发送的请求，对请求进行处理，并响应数据Service:业务逻辑层，处理具体的业务逻辑Dao:数据访问层，复杂数据访问操作，包括数据的增删改查Bean实例化实例化Bean的方式用构造方法(无参)创建对象，如果没有无参构造方法将抛出异常BeanCreationException静态工厂实例化Bean， 实例工厂初始化Bean 可以使用接口FactoryBean<>来代替public class UseDaoFactoryBean implements FactoryBean<UserDao>{ public UserDao getObject() throws Exception{ return new UserDaoImpl(); } public Class<> getObjectType(){ reuturn UserDao.class; } }配置 Bean的生命周期生命周期：从创建到销毁的完整过程初始化容器创建对象执行构造方法执行属性注入(set)执行bean初始化方法使用bean关闭/销毁容器执行bean销毁方法bean的生命周期控制：在bean创建后到销毁前做一些事情加载properties文件开启context命名空间<? xml version="1.0" encoding="UTF-8"?> < beans xmlns=" http://www.springframework.org/schema/bean s" xmlns: xsi=" http://www.w3.org/2001/XMLSchema-instance " xmlns: context=" http://www.springframework.org/schema/context " xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd "> </ beans>使用contex命名空间<context:property-placeholder locaion=" "/>使用${}读取加载的属性值 不加载系统属性<context:property-placeholder location="" system-properties-mode="NEVER"/>加载多个properties文件<context:property-placeholder location="jdbc.properties.msg.properties"/>加载所有properties文件<context:property-placeholder location="*.properties"/>标准格式<context:property-placeholder location="classpath:*.properties"/>从类路径或jar包中搜索并加载properties文件<context:property-placeholder location="classpath*:*.properties"/>依赖注入向一个类传递数据的方式普通方法(set方法)构造方法bean运行需要的数据类型可以简单地分成两大类引用类型简单类型(基本数据类型与String)Setter注入引用类型在bean中定义引用类型属性并提供可访问地set方法public class BookServiceImpl implements BookService{ private BookDao bookDao; public void setBookDao(BookDao bookDao){ this.bookDao = bookDao; } }配置中使用property标签ref属性注入引用对象 简单类型在bean中定义引用类型属性并提供可访问地set方法public class BookDaoImpl implements BookDao{ private int connectionNumber; public void setConnectionNumber(int connectionNumber){ this.connectionNumber = connectionNumber; } }配置中使用property标签value属性注入引用对象 构造方法注入引用类型public class BookServiceImpl implements BookService{ private int connectionNumber; public BookServiceImpl(BookDao bookDao){ this.bookDao=bookDao; } } <constructor-arg name="形参的名称" ref="bookDao"/> 简单类型 <constructor-arg name="形参的名称" value="xxx"/> 但是依然有耦合，另外一种写法type中写对应参数的类型，解决形参名的问题 <constructor-arg type="java.lang.String" value="xxx"/> index写参数的位数，第0个，第1个...解决参数类型重复的问题 <constructor-arg index="0" type="java.lang.String" value="xxx"/> 依赖注入方式的选择强制依赖：bean运行必须的依赖强制依赖使用构造器进行，使用setter注入又改了不进行注入导致null对象出现可选以来用setter注入，灵活Spring框架倡导使用构造器，第三方框架内部大多采用构造器注入的形式进行初始化如有必要可以两者同时使用，使用构造器注入完成强制依赖输入，setter注入完成可选依赖注入实际开发时要根据实际情况分析，如果受控对象没有提供setter方法就必须使用构造器注入自己开发的模块推荐使用setter注入依赖自动装配IoC容器根据bean所以来的资源在容器中自动查找并注入到bean中的过程称为自动装配自动装配方式按类型(常用)按名称按构造方法autowire标签后面可以选择不同的方式使用byType，如果存在多个bean类型相同会报错，此时可以使用byName 提示自动装配用于引用类型依赖注入，不能对简单类型进行操作使用按类型装配(byType)必须保证容器中相同类型的bean唯一使用按名称装配(byName)必须保证容器中右指定名称的bean，但是资源变量名与配置会耦合自动装配优先级低于setter注入和构造器注入，提示出现时自动装配配置失效集合注入数组 List Set Map Propertiesname属性后面是变量名，和类型无关 100 200 ahsoi sakjahfkj ahsoi sakjahfkj <entry key="country" value="china"/> china 注解开发使用注解配置bean@Component("bean名称")要在核心配置文件中通过组件扫描加载bean<context:component-scan base-package=" "/>业务层的bean建议使用@Service数据层使用@Respository表现层使用@Controller纯注解开发用类来代替配置文件，使用注解@Configuration(设定当前类为配置类)用注解@ComponentScan("")来设定扫描路径，次注解只能加一次//转换前 ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml") //转换后，写的是配置类的名称 ApplicationContext ctx = new ClassPathXmlApplicationContext(SpringConfig.class)bean管理bean作用范围@Scope注解bean生命周期@PostConstruct注解，表示在构造方法后运行@PreDestroy注解，表示在销毁前运行注解开发依赖注入自动装配@Autowired注解，可以把set方法去掉如果有两个相同类型bean的数据层实现要用名称指定@Qualifier("名称")+@Autowired(前者必须配合后者使用)注：自动装配基于反射设计创建对象并暴力发射对应属性为私有属性初始化数据，无需提供setter方法自动装配建议使用无参构造方法创建对象简单类型注入使用@Value("值")注解@Value("sjdh") private String name;可以从properties文件中加载配置，需要在对应的Config类里面加上@PropertySource注解@Configuration @ComponentScan("...") //不能使用通配符* @PropertySource("xxx.properties") public class Config(){ }第三方bean管理需要手动获得管理对象public class SpringConfig{ //定义一个方法要管理的对象 //添加注解，表示返回值是bean @Bean public DataSource datasource(){ DruidDataSource ds=new DruidDataSource(); ds.setDriverClassName(" "); return ds; } }方式一：导入式public class SpringConfig{ //定义一个方法要管理的对象 //添加注解，表示返回值是bean @Bean public DataSource datasource(){ } }使用@Import主角儿手动加入配置类到核心配置，只能添加一次@Configuration @Import() public class SpringConfig{ }方式二：扫描式(不建议)第三方bean依赖注入引用类型注入只需要为bean定义方法设置形参即可，容器会根据类型自动装配对象@Bean public DataSource dataSource(BookService bookService){ System.out.println(bookService); DruidDataSource ds = new DruidDataSource(); return ds; }AOPAOP核心概念AOP(Aspect Oriented Programming)面向切面编程，一种编程范式，指导开发者如何组织程序结构作用：在不动原始设计的基础上增强功能Spring理念：无入侵式抽取出来的方法称为连接点，要追加功能的方法称为切入点，共性功能称为通知，用切面描述通知与切入点的关系还要有通知类，连接点式所有方法，切入点是匹配某些方法连接点(JoinPoint)：程序执行过程中的任意位置在SpringAOP中理解为方法的执行切入点(Pointcut)：匹配连接点的式子通知(Advice)：在切入点处执行的操作，也就是共性功能切面(Aspect)：描述通知与切入点的对应关系AOP思路将坐标导入pom.xml文件 org.aspectj aspectjweaver 1.9.4 制作连接点方法（原始操作，Dao接口与实现类）public interface BookDao{ public void save(); public void update(); }@Repository public class BookDaoImpl implements BookDao{ public void save(){ System.out.println(System.currentTimeMillis()); System.out.println("book dao save ... "); } public void update(){ System.out.println("book dao update ... "); } }定义通知类public void MyAdvice{ public void before(){ System.out.println(System.currentTimeMillis()); } }定义切入点@Pointcut注解public void MyAdvice{ @Pointcut("executio()") private void pt(){} public void before(){ } }绑定切入点与通知关系（切面）public void MyAdvice{ @Pointcut("execution()") @Before("pt()") public void before(){ } }定义通知类受Spring管理，并定义当前类为切面类要使用@Aspect和@Component注解AOP工作流程Spring容器启动读取所有切面配置中的切入点初始化bean，判顶对应的类中的方法是都撇皮到任意切入点​ 如果匹配失败则创建对象​ 匹配成功，创建目标对象的代理对象获取bean执行方法​ 获取bean，调用方法并执行，完成操作​ 获取的bean式代理对象时，根据代理对象的运行模式运行原始方法与增强的内容，完成操作目标对象原始功能去掉共性功能对应的类产生的对象，这种对象无法直接完成最后的工作代理目标对象无法直接完成工作，需要对其进行功能回填，通过原始对象的代理对象实现AOP切入点表达式切入点：要进行增强的方法切入点表达式：要进行增强的方法的描述方式描述方式一：执行该路径下BookDao接口中的无参数update方法execution(void.com.it.dao.BookDao.update())描述方法二：执行impl包下的BookDaoImpl类中的无参数update方法execution(void com.it.dao.impl.BookDaoImpl.update())切入点表达式标准动作关键字（访问修饰符 返回值 包名.类/接口名.方法名(参数)异常名）可以使用通配符描述切入点*：单个独立的任意符号，可以独立出现，也可以作为前缀或者后缀的匹配符出现execution (public * com. itheima.*. UserService. find* (*))匹配 com. itheima包下的任意包中的UserService类或接口中所有 find开头的带有一个参数的方法..：多个连续的任意符号，可以独立出现，常用于简化包名与参数的书写execution (public User com.. UserService. findById (..))匹配 com包下的任意包中的UserService类或接口中所有名称为findByld的方法+：专用于匹配子类型execution(*..* Service+.*(..))书写技巧描述切入点通常描述接口，而不描述实现类包名书写尽量不使用..匹配方法名书写档次以进行精准匹配通常不使用异常作为匹配接口名/类名书写名称与模块相关的采用*匹配AOP通知类型分类前置通知@Before("")后置通知@After("")环绕通知@Around("")返回后通知@AfterReturning抛出异常后通知@AfterThrowing@Around注意事项环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用，进而实现原始方法调用前后同时添加通知如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行如果对原始方法接受返回值，设置成Object类型环绕通知方法必须抛出Throwable对象AOP通知获取数据获取切入点方法的参数JoinPoint：适用于前置，后置，返回后、抛出异常后通知@Berfore() public void before(JoinPoint jp){ Object[] args=jp.getArgs(); }ProceedJointPoint：适用于环绕通知，是JoinPoint的子类获取切入点方法返回值返回后通知@AfterReturning(value="",retyrning="形参名")环绕通知获取切入点方法运行异常异常抛出异常后通知环绕通知Spring事务作用：在数据层或业务层保障一系列的数据库操作痛失败痛成功通过PlatformTransactionManager接口实现里面有commit和rollback方法在业务层接口上添加Spring事务管理@Transactional注解(通常添加在业务层接口而不是业务层实现类)设置事务管理器(JDBC事务，根据实际情况选择)@Bean public PlatformTransactionManager transactionManager(DataSource dataSource){ DataSourceTransactionManager ptm = new DataSourceTransactionManager(); ptm.setDataSource(dataSource); return ptm; }开启注解式事务驱动在SpringConfig类上添加@EnableTransactionManagement注解事务角色事务管理员：发起事务方，在Spring中通常指代业务层开启事务的方法事务协调员：加入事务方，在Spring中通常指代数据层方法，也可以是业务层方法事务属性事务相关配置readOnly，rollbackFor等