首页
壁纸
统计
友链
Search
1
Vue2详细笔记
37 阅读
2
Nestjs概述-中文
19 阅读
3
ExpressAPI
17 阅读
4
答辩
14 阅读
5
JavaScript企业数据处理实用指南
13 阅读
Nodejs
Vue
Java
Msql
登录
Search
Wasnl
累计撰写
36
篇文章
累计收到
1
条评论
首页
栏目
Nodejs
Vue
Java
Msql
页面
壁纸
统计
友链
搜索到
16
篇与
的结果
2026-06-02
未命名文档
注意:Java 8 环境下,javax.annotation(@PostConstruct、@PreDestroy、@Resource)是 JDK 自带的,无需额外添加依赖。Spring Boot 2.7 使用 Spring Framework 5.x,完美支持 JSR-250 规范。一、Bean 的作用域(Scope)Spring 中的 Bean 默认是 单例(Singleton),即整个容器内只有一个实例。也可以通过 @Scope 指定为 原型(Prototype),每次获取都创建新实例。1.1 Singleton(单例)—— 默认创建时机:容器启动时(除非设置为 @Lazy)。销毁时机:容器正常关闭时,会调用 @PreDestroy 方法。获取结果:每次注入或 getBean() 返回同一个对象。@Component // 默认 singleton public class MailService { // ... }1.2 Prototype(原型)行为:每次获取(@Autowired 或 getBean)都会创建一个全新的实例。生命周期差异:容器负责创建 → 依赖注入 → 调用 @PostConstruct。容器不负责销毁:即使你在原型 Bean 中写了 @PreDestroy,Spring 不会自动调用它。需要你自己写代码清理(例如手动调用销毁方法)。声明方式:@Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // 或 @Scope("prototype") public class MailSession { @PostConstruct public void init() { System.out.println("原型 Bean 初始化,每次都会调用"); } @PreDestroy public void destroy() { System.out.println("原型 Bean 销毁 — 不会被 Spring 自动调用"); } }最佳实践:如果没有特殊需求,优先使用 singleton。prototype 适合那些有状态且不宜共享的对象,但要记得手动管理资源释放。二、注入 List —— 自动收集同一类型的所有 Bean当你需要在一个地方使用某个接口的 所有实现类 时,可以直接注入 List<T>,Spring 会自动把该接口的所有 Bean 放进列表。示例:多个 Validator 实现public interface Validator { void validate(String email, String password, String name); } @Component // 自动被收集 public class EmailValidator implements Validator { @Override public void validate(String email, String password, String name) { // email 校验逻辑 } } @Component public class PasswordValidator implements Validator { @Override public void validate(String email, String password, String name) { // password 校验逻辑 } } @Component public class NameValidator implements Validator { @Override public void validate(String email, String password, String name) { // name 校验逻辑 } }注入 List@Component public class Validators { @Autowired private List<Validator> validators; // Spring 自动注入所有 Validator 实现 public void validate(String email, String password, String name) { for (Validator v : validators) { v.validate(email, password, name); } } }控制顺序 —— @Order默认顺序不确定,可以用 @Order 注解指定(数字越小优先级越高)。@Component @Order(1) public class EmailValidator implements Validator { ... } @Component @Order(2) public class PasswordValidator implements Validator { ... } @Component @Order(3) public class NameValidator implements Validator { ... }注入的 List<Validator> 会按照 @Order 的值升序排列。三、可选注入 —— @Autowired(required = false)默认情况下,如果某个依赖的 Bean 不存在,Spring 会抛出 NoSuchBeanDefinitionException。如果你允许该依赖为 null,可以设置 required = false。@Component public class MailService { @Autowired(required = false) private ZoneId zoneId = ZoneId.systemDefault(); // 找不到 ZoneId 时使用默认值 }典型场景:某些配置是可选的(如时区、外部服务),若有定义则使用,否则退回到默认值。四、创建第三方 Bean —— @Bean 方法对于不在我们包内的类(例如 JDK 的 ZoneId、第三方库的 DataSource),无法用 @Component 扫描。此时需要在 @Configuration 类中编写一个 工厂方法,并标记 @Bean。@Configuration @ComponentScan public class AppConfig { @Bean public ZoneId zoneId() { return ZoneId.of("Z"); } @Bean("customRestTemplate") // 指定 Bean 名称 public RestTemplate myRestTemplate() { return new RestTemplate(); } }Spring 保证该方法 只被调用一次(默认单例)。方法名默认作为 Bean 的名称,也可用 @Bean("name") 显式命名。可以依赖其他 Bean:方法参数会自动注入。@Bean public DataSource dataSource() { return HikariDataSource(...); } @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { // 自动注入上面创建的 DataSource return new JdbcTemplate(dataSource); }本质上,@Bean 方法就是一种 工厂方法,比实现 FactoryBean 接口更简洁。五、Bean 的初始化和销毁 —— @PostConstruct 和 @PreDestroy5.1 为什么需要初始化:在依赖注入完成后,执行一些启动逻辑(打开连接、启动后台线程、校验配置)。销毁:在容器关闭前,释放资源(关闭连接池、停止线程、清理临时文件)。5.2 使用方式(JSR-250,Java 8 原生支持)@Component public class MailService { @Autowired(required = false) private ZoneId zoneId = ZoneId.systemDefault(); @PostConstruct public void init() { System.out.println("初始化:依赖已注入,zoneId = " + zoneId); } @PreDestroy public void shutdown() { System.out.println("销毁前:释放资源(如关闭连接池)"); } }5.3 执行顺序初始化流程:调用构造方法创建实例依赖注入(@Autowired、@Resource 等)调用 @PostConstruct 方法(如果实现了 InitializingBean)调用 afterPropertiesSet()(如果配置了 init-method)调用自定义方法销毁流程(容器正常关闭时):调用 @PreDestroy 方法(如果实现了 DisposableBean)调用 destroy()(如果配置了 destroy-method)调用自定义方法5.4 重要注意事项方法必须 无参数,返回值任意(通常 void)。方法名任意,不要求 init/destroy。prototype 作用域的 Bean,Spring 只调用 @PostConstruct,不会自动调用 @PreDestroy。如果原型 Bean 需要清理,你必须自己设计清理逻辑(例如提供 cleanup() 方法并手动调用)。5.5 “全自动”的条件在 Spring Boot 2.7(Java 8)中,只要满足以下条件,@PostConstruct 和 @PreDestroy 就是 全自动 的:Bean 是 singleton 作用域(默认)。Spring Boot 应用正常关闭(按 Ctrl+C、收到 SIGTERM、程序正常结束)。Spring Boot 会自动注册 JVM 关闭钩子,触发容器关闭,从而调用 @PreDestroy。不要使用 kill -9 强行杀死进程(那样 JVM 直接退出,不会执行关闭钩子)。@SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); // 按 Ctrl+C 时,会看到 @PreDestroy 的输出 } }六、处理多个同类型 Bean —— 别名、限定符、主次6.1 问题描述在 @Configuration 中定义多个同类型的 @Bean:@Configuration public class AppConfig { @Bean ZoneId createZoneOfZ() { return ZoneId.of("Z"); } @Bean ZoneId createZoneOfUTC8() { return ZoneId.of("UTC+08:00"); } }直接注入 ZoneId 会报错:NoUniqueBeanDefinitionException。6.2 解决方案(1) 为每个 Bean 起一个名字(别名)@Bean("z") ZoneId createZoneOfZ() { ... } @Bean @Qualifier("utc8") ZoneId createZoneOfUTC8() { ... }(2) 注入时用 @Qualifier 指定名称@Component public class MailService { @Autowired @Qualifier("z") // 精确指定名称为 "z" 的那个 ZoneId private ZoneId zoneId; }或者使用 JSR-250 的 @Resource(name = "z"):@Component public class MailService { @Resource(name = "z") private ZoneId zoneId; }(3) 使用 @Primary 指定主要 Bean当没有使用 @Qualifier 时,Spring 会注入标记 @Primary 的那个 Bean。@Bean @Primary // 主要候选 @Qualifier("z") ZoneId createZoneOfZ() { ... } @Bean @Qualifier("utc8") ZoneId createZoneOfUTC8() { ... }注入时:@Component public class MailService { @Autowired // 无 @Qualifier,默认注入 @Primary 的那个(即 "z") private ZoneId zoneId; }6.3 经典应用:多数据源@Configuration public class DataSourceConfig { @Bean @Primary DataSource masterDataSource() { ... } @Bean @Qualifier("slave") DataSource slaveDataSource() { ... } }默认注入主数据源(如 DAO 层无需特殊指定)。需要从库时:@Qualifier("slave") 精确注入。七、FactoryBean —— 工厂模式创建 Bean(了解即可)7.1 是什么FactoryBean 是 Spring 提供的工厂接口,允许你自定义 Bean 的创建逻辑。Spring 会先创建 FactoryBean 实例,再调用其 getObject() 方法返回真正的 Bean。@Component public class ZoneIdFactoryBean implements FactoryBean<ZoneId> { private String zone = "Z"; @Override public ZoneId getObject() throws Exception { return ZoneId.of(zone); } @Override public Class<?> getObjectType() { return ZoneId.class; } }注入/获取时拿到的是 getObject() 返回的 ZoneId。如果需要获取 FactoryBean 本身,可以在名称前加 &:context.getBean("&zoneIdFactoryBean")。7.2 现状在 Spring Boot 2.7 中,@Bean 方法已经足够简洁强大,完全可以替代 FactoryBean 的大部分场景。除非你要编写非常复杂的创建逻辑,或需要与旧框架集成(如 MyBatis 的 MapperFactoryBean),否则推荐直接使用 @Bean 方法。八、综合对比与最佳实践功能推荐方案关键点普通 Bean@Component默认 singleton原型 Bean@Scope("prototype")每次新建,容器不负责 @PreDestroy收集所有实现@Autowired List<Interface>配合 @Order 控制顺序可选依赖@Autowired(required=false)避免异常,可设默认值第三方 Bean@Bean 方法在 @Configuration 中定义初始化逻辑@PostConstruct依赖注入后自动执行销毁逻辑@PreDestroy容器关闭时自动执行(singleton 有效)同类型多个 Bean@Qualifier + 命名精确注入;@Primary 设置默认复杂工厂@Bean 方法一般不用 FactoryBean九、Java 8 + Spring Boot 2.7 特别提醒注解包:@PostConstruct、@PreDestroy、@Resource 使用 javax.annotation,无需添加任何额外依赖。Spring Boot 自动关闭:Spring Boot 应用默认注册 JVM 关闭钩子,只要你正常停止应用(Ctrl+C、kill 不带 -9、System.exit()),@PreDestroy 就会被调用。prototype 的陷阱:如果你的 Bean 是 prototype 且需要释放资源,请考虑改用 singleton,或自行设计清理方法(如实现 DisposableBean 并手动调用)。@Bean 方法的默认名称:方法名即 Bean 名称,注意避免重名(同一类型不同方法名)。
2026年06月02日
3 阅读
0 评论
0 点赞
2026-06-02
Java随心记
一、常用工具与集合操作1.1 Arrays.asList()作用:将数组转换为 List。注意:返回的 List 是固定大小的,不支持 add/remove 操作。List<String> list = Arrays.asList("a", "b", "c");1.2 CollUtil.toMap()(Hutool 工具)把一个 List 转换成 Map。参数1:要转换的列表。参数2:key 的生成方式(如 Stock::getSkuId)。参数3:value 的生成方式,Function.identity() 表示用对象本身作为 value。Map<Long, Stock> map = CollUtil.toMap(stocks, Stock::getSkuId, Function.identity());1.3 IdentityHashMap比较冷门,判断 key 是否相同依据 对象的引用地址(==),而不是 equals() / hashCode()。二、数据查询策略2.1 联表查询 vs 分开查 + Map 组装方式适用场景联表查询数据量不大、关联简单、不需要对查出的用户数据做额外处理分开查 + Map 组装数据分布在不同的服务器、微服务架构,或者需要灵活组装三、Java 集合框架3.1 集合体系概览Collection 是除 Map 外所有集合类的根接口。java.util 包主要提供三种类型的集合:List、Set、Map。统一使用 Iterator 遍历集合。3.2 ListArrayList:基于动态数组,内存连续。LinkedList:基于双向链表,节点有前后指针。List 允许添加 null。使用 List.of() 创建不可变 List(不支持 null 元素)。List<Integer> list = List.of(1, 2, 5);Iterator 遍历:boolean hasNext() 判断是否有下一个元素。E next() 返回下一个元素。boolean contains(Object o) 判断是否包含某元素。int indexOf(Object o) 返回元素索引,不存在返回 -1。重要:使用 contains()、indexOf() 等方法时,元素类必须正确覆写 equals()。String、Integer 等标准库类已经正确实现了 equals(),所以可以直接使用。3.3 编写 equals() 方法理论原则:自反性、对称性、传递性、一致性。实践写法(直接套用):先确定实例相等的逻辑(哪些字段相等就算相等)。用 instanceof 判断类型,类型匹配后再比较字段。使用 Objects.equals() 比较引用类型字段,避免空指针。public boolean equals(Object o) { if (o instanceof Person p) { return Objects.equals(this.name, p.name) && this.age == p.age; } return false; }3.4 MapMap 遍历是无序的。底层是哈希表,通过哈希函数将 key 转换成数组下标,直接定位,查找速度极快 O(1)。作为 key 的对象必须正确覆写 equals() 和 hashCode(),相等的 key 调用 equals 必须返回 true。3.5 Queue 队列实现先进先出(FIFO)的数据结构。常用方法:add() / offer():添加元素到队尾。remove() / poll():从队首获取并删除元素。element() / peek():从队首获取但不删除。避免将 null 添加到队列。四、编码与安全4.1 URL 编码使用 java.net.URLEncoder 对任意字符串进行 URL 编码。4.2 哈希算法目的:验证原始数据是否被篡改。MessageDigest 常见用途:计算文件校验和实现文件秒传存储用户密码防止数据重复生成 API 签名4.3 AES 加密最可靠的 AES 加密模式:AES-GCM。五、多线程与并发5.1 线程基础守护线程(Daemon)JVM 退出时不等待守护线程结束,直接强制终止。非守护线程(用户线程)结束时 JVM 才会退出。设置方法:在 start() 前调用 setDaemon(true)。核心业务线程不能设置为守护线程。典型用途:垃圾回收、日志监控、心跳检测。t.join()让当前线程等待 t 线程执行结束。生产环境禁用,容易造成阻塞,推荐用 Future 等异步方式。Thread.sleep(long millis)静态方法,哪个线程调用,哪个线程睡觉。线程中断线程结束即 run() 方法执行完毕。目标线程通过检查 isInterrupted() 标志判断自身是否已被中断。volatile 与内存可见性变量的值保存在主内存,线程访问时会拷贝到工作内存。修改后的值何时回写主内存不确定,因此会出现可见性问题。synchronized 不能修饰变量,要保证变量跨线程可见性,使用 volatile。注意:volatile 只能保证可见性和禁止指令重排,不保证原子性。5.2 同步机制synchronizedJava 语言层面语法,不需要手动释放锁(退出同步块自动释放)。修饰方法时,锁对象是 this(实例方法)或类对象(静态方法)。配合 wait() / notifyAll() 实现线程等待/唤醒(了解即可,实际多用 Lock 体系)。ReentrantLockJava 代码实现的锁,必须在 finally 中手动释放。标准用法:private final Lock lock = new ReentrantLock(); lock.lock(); try { // 同步代码 } finally { lock.unlock(); }配合 Condition 的 await() / signalAll() 实现等待/通知。ReadWriteLock提升读多写少场景的并发性能。规则:读-读 可共存写-写 互斥读-写 互斥(要么全是读,要么只有一个写)实现:private final ReadWriteLock rwlock = new ReentrantReadWriteLock(); private final Lock rlock = rwlock.readLock(); private final Lock wlock = rwlock.writeLock(); // 写操作 wlock.lock(); try { ... } finally { wlock.unlock(); } // 读操作 rlock.lock(); try { ... } finally { rlock.unlock(); }注意:读锁是悲观锁,频繁读可能导致写线程饥饿。StampedLock三种模式:写锁、悲观读锁、乐观读。乐观读 tryOptimisticRead() 完全不阻塞写线程,先获取版本号再读数据,读完后验证版本号有无变化,变化则重试或升级为悲观读。不可重入(一个线程持有写锁再尝试获取读锁会死锁)。Semaphore信号量,限制同时访问某个资源的线程数量。用法:final Semaphore semaphore = new Semaphore(3); // 最多3个许可 public String access() throws Exception { semaphore.acquire(); // 获取许可,可能阻塞 try { // 业务逻辑 } finally { semaphore.release(); // 释放许可 } }tryAcquire(long timeout, TimeUnit unit) 可指定等待时间。5.3 线程安全集合接口非线程安全线程安全替代ListArrayListCopyOnWriteArrayListMapHashMapConcurrentHashMapSetHashSet / TreeSetCopyOnWriteArraySetQueueArrayDeque / LinkedListArrayBlockingQueue / LinkedBlockingQueueDequeArrayDeque / LinkedListLinkedBlockingDeque5.4 线程池ExecutorService经典用法:ExecutorService executor = Executors.newFixedThreadPool(3); executor.submit(task1); ... executor.shutdown(); // 等待任务完成后关闭常用线程池及底层原理:FixedThreadPool:核心线程数 = 最大线程数 + 无界队列CachedThreadPool:核心线程数 0,最大线程数无限 + 同步移交队列SingleThreadExecutor:核心和最大线程数均为 1 + 无界队列ScheduledThreadPool:ScheduledThreadPoolExecutor,支持定时调度生产推荐:直接使用 ThreadPoolExecutor 定制参数,队列必须有界。public ThreadPoolExecutor( int corePoolSize, // CPU密集:n+1 IO密集:2n int maximumPoolSize, // ≥ corePoolSize long keepAliveTime, // 非核心线程空闲存活时间,常用30-60s TimeUnit unit, // 时间单位 BlockingQueue<Runnable> workQueue, // 有界队列,如 ArrayBlockingQueue ThreadFactory threadFactory, // 线程工厂 RejectedExecutionHandler handler // 拒绝策略 )拒绝策略策略行为AbortPolicy(默认)丢弃任务,抛 RejectedExecutionExceptionCallerRunsPolicy由提交任务的线程自行执行,可减缓提交速度(推荐)DiscardPolicy静默丢弃任务,不抛异常DiscardOldestPolicy丢弃队列最老的任务,重新提交当前任务推荐配置:CallerRunsPolicy + 监控告警。注意事项创建线程只有一种方式:new Thread().start()。生产环境只用线程池提交任务,禁止手动 new Thread()。for 循环中调用 t.start() 会立即返回,不会等待线程 run() 执行完。5.5 Future 与异步编程Callable 与 Runnable 的区别:Callable 有返回值,可抛出异常。提交 Callable 返回 Future<V>。get():获取结果(可能阻塞)get(long timeout, TimeUnit unit):限时获取cancel(boolean mayInterruptIfRunning):取消任务isDone():判断任务是否完成CompletableFuture实现 Future 和 CompletionStage,支持函数式异步编程。方法引用示例:Main::fetchPrice 等价于 () -> fetchPrice(),常用在 CompletableFuture.supplyAsync(Main::fetchPrice) 中。六、JDBC 简介各数据库厂商使用统一接口,Java 代码无需针对不同数据库分别开发。Maven 依赖:<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> <scope>runtime</scope> <!-- 运行时才需要此包 --> </dependency>建立连接:DriverManager.getConnection()(自动扫描 classpath 中的驱动)。重要提示:数据库自身就是并发专家,不要用 Java 层锁去干涉数据库操作,除非操作对象是内存数据。
2026年06月02日
4 阅读
0 评论
0 点赞
2026-06-02
Java 反射与动态代理 详细笔记
Java 反射与动态代理 详细笔记一、反射的核心思想反射:在程序运行时,动态获取某个类的完整信息(类名、方法、字段、父类、接口等),并能动态创建对象、调用方法、访问字段。不需要在编译时知道具体类名,可以在运行时根据字符串类名加载类。常用于框架(Spring、MyBatis)、通用工具(IDE 自动补全)、动态代理等。二、Class 类:一切反射的入口2.1 每个类都有唯一的 Class 对象JVM 在第一次加载一个类时,会在内存中为该类创建一个 Class 实例(俗称“说明书”)。这个 Class 实例保存了该类的全部信息(类名、包、方法、字段、父类、接口等)。无论通过哪种方式获取,同一个类的 Class 实例是同一个对象(可用 == 比较)。2.2 获取 Class 实例的三种方法// 方法1:通过类名.class(最直接,编译时已知类) Class<String> cls1 = String.class; // 方法2:通过实例的 getClass() 方法 String s = "hello"; Class<?> cls2 = s.getClass(); // 实际返回 String.class // 方法3:通过 Class.forName(完整类名)(运行时根据字符串加载) Class<?> cls3 = Class.forName("java.lang.String");三者获得的是同一个 Class 对象:cls1 == cls2 == cls3三、通过反射访问字段(Field)3.1 获取字段的方法(返回值是 java.lang.reflect.Field)方法说明能否得到父类字段getField(String name)获取 public 字段(包括父类)✅ 能getDeclaredField(String name)获取当前类声明的任意字段(不限访问修饰符)❌ 不能,只看当前类getFields()获取所有 public 字段(包括父类)✅ 能getDeclaredFields()获取当前类声明的所有字段(不包括父类)❌ 不能3.2 访问私有字段通过 getDeclaredField 拿到私有 Field 对象后,调用 setAccessible(true) 打破封装。然后可以 get(obj) / set(obj, value) 读写该私有字段。class Person { private String name; } Class<Person> cls = Person.class; Field field = cls.getDeclaredField("name"); field.setAccessible(true); // 允许访问私有字段 Person p = new Person(); field.set(p, "Alice"); String name = (String) field.get(p); // "Alice"四、通过反射调用方法(Method)4.1 获取方法的方法(返回值是 java.lang.reflect.Method)方法说明包括父类getMethod(String name, Class<?>... parameterTypes)获取某个 public 方法✅ 包括getDeclaredMethod(...)获取当前类声明的某个方法(不限修饰符)❌ 不包括getMethods()获取所有 public 方法✅ 包括getDeclaredMethods()获取当前类声明的所有方法❌ 不包括4.2 调用方法拿到 Method 对象后,调用 invoke(Object obj, Object... args)。如果方法是 static,第一个参数传 null。// 示例:调用 String 的 length() 方法 String str = "hello"; Method m = String.class.getMethod("length"); int len = (int) m.invoke(str); // len = 5五、通过反射调用构造器(Constructor)5.1 获取构造器方法说明getConstructor(Class<?>... parameterTypes)获取某个 public 构造器getDeclaredConstructor(...)获取任意修饰符的构造器(包括 private)getConstructors() / getDeclaredConstructors()获取所有构造器5.2 创建实例通过 Constructor.newInstance(Object... initargs) 创建对象。比 Class.newInstance() 更灵活(可以传参数,可以调用非 public 构造器)。// 调用带参数的构造器 Constructor<String> cons = String.class.getConstructor(String.class); String s = cons.newInstance("abc"); // 相当于 new String("abc") // 调用私有构造器(单例模式等) Constructor<MyClass> cons2 = MyClass.class.getDeclaredConstructor(); cons2.setAccessible(true); MyClass obj = cons2.newInstance();注意:Class.newInstance() 已过时(只能调用无参 public 构造器),推荐使用 Constructor.newInstance()。六、获取继承关系方法作用示例Class.getSuperclass()返回父类的 Class 对象Integer.class.getSuperclass() → Number.classClass.getInterfaces()返回当前类直接实现的接口数组Integer.class.getInterfaces() → Comparable, Constable, ConstantDescClass.isAssignableFrom(Class<?> cls)判断当前类是否可接收 cls 类型的对象(即 cls 能否向上转型为当前类)Number.class.isAssignableFrom(Integer.class) → true6.1 关于接口的特殊说明对接口调用 getSuperclass() → 返回 null。获取接口的父接口必须用 getInterfaces()。接口的 isAssignableFrom 用法与类一致:Serializable.class.isAssignableFrom(Integer.class) → true。6.2 instanceof 与 isAssignableFrom 的区别instanceof 需要对象实例:obj instanceof NumberisAssignableFrom 需要Class 对象:Number.class.isAssignableFrom(Integer.class)七、动态代理(Dynamic Proxy)7.1 解决的问题不手动编写接口的实现类,就能在运行时动态创建实现了该接口的对象。典型应用:AOP(面向切面编程)、RPC 框架、日志/事务拦截等。7.2 限制条件只能代理接口(不能代理普通类)。如果需要代理普通类,可使用 CGLIB 或 ByteBuddy。7.3 核心 APIjava.lang.reflect.Proxyjava.lang.reflect.InvocationHandler(函数式接口)7.4 实现步骤定义接口(必须,告知代理对象有哪些方法)。实现 InvocationHandler:重写 invoke 方法,定义当接口方法被调用时实际执行的代码。调用 Proxy.newProxyInstance 创建代理实例。将代理实例转型为接口类型,然后正常调用方法。7.5 示例代码(完整)// 1. 接口 public interface Hello { void morning(String name); } // 2. 使用动态代理(不写 HelloWorld 类) import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class DynamicProxyDemo { public static void main(String[] args) { // 2.1 定义调用处理器 InvocationHandler handler = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("morning")) { String name = (String) args[0]; System.out.println("Good morning, " + name); } return null; // 方法返回 void,所以返回 null } }; // 2.2 创建代理对象 Hello hello = (Hello) Proxy.newProxyInstance( Hello.class.getClassLoader(), // 类加载器 new Class[]{Hello.class}, // 要实现的接口列表 handler // 调用处理器 ); // 2.3 调用方法(实际执行 handler.invoke) hello.morning("Bob"); // 输出:Good morning, Bob } }7.6 动态代理内部原理Proxy.newProxyInstance 会在运行时动态生成一个新的类(例如 $Proxy0)。这个类实现了指定的接口(Hello),并且每个方法的实现都是:public void morning(String name) { super.h.invoke(this, method_morning, new Object[]{name}); }super.h 就是你传入的 InvocationHandler。然后通过 ClassLoader 加载这个类,并实例化返回。换句话说:动态代理 = JVM 自动帮你写了一个实现类,并用反射调用你的 handler。7.7 动态代理与反射的关系动态代理生成的新类($Proxy0)本身也是一个类,JVM 会为它创建 Class 对象。你可以对这个 Class 对象进行任何反射操作。InvocationHandler.invoke 方法中收到的 Method 参数就是通过反射得到的。所以动态代理是反射机制的一个典型应用,而不是替代品。八、总结:什么时候用什么技术?场景推荐技术已知类名,需要调用它的方法/访问字段直接调用,无需反射类名字符串在运行时才确定,需要创建实例Class.forName() + Constructor.newInstance()需要遍历一个对象的所有字段/方法(例如写序列化工具)getDeclaredFields() / getDeclaredMethods()不写实现类,仅通过接口定义行为动态代理需要在方法调用前后统一添加逻辑(日志、事务)动态代理 + InvocationHandler最后记住一句话:反射让你在运行时“看见”并“操作”一个类;动态代理让你在运行时“凭空造出”一个实现了接口的类,并把所有方法调用委托给你写的处理器。
2026年06月02日
7 阅读
0 评论
0 点赞
2026-04-10
JUnit 5 单元测试完整笔记
JUnit 5 单元测试完整笔记一、JUnit 5 概述JUnit 5 是 Java 生态中最主流的单元测试框架,提供丰富的注解、断言和扩展机制。1.1 框架构成(三个子项目)子项目说明JUnit Platform在 JVM 上启动测试框架的基础平台,定义了 TestEngine APIJUnit Jupiter包含 JUnit 5 全新的编程模型和扩展机制JUnit Vintage提供对 JUnit 3 和 JUnit 4 测试的兼容运行支持二、核心测试注解JUnit 5 通过注解控制测试执行流程与行为。注解说明使用场景@Test标记一个方法为测试方法基本单元测试@BeforeEach在每个测试方法执行前运行测试数据准备、对象初始化@AfterEach在每个测试方法执行后运行资源清理、状态重置@BeforeAll在当前测试类的所有测试方法执行前运行一次(需为 static)类级别初始化(如建立数据库连接)@AfterAll在所有测试方法执行后运行一次(需为 static)类级别资源释放@DisplayName为测试类或方法自定义易读的显示名称提升测试报告的可读性@Disabled临时禁用某个测试方法或类跳过尚未实现或需要暂时忽略的测试@ParameterizedTest声明参数化测试方法使用多组数据驱动同一测试逻辑@RepeatedTest重复执行指定次数的测试验证方法稳定性或简单压力测试@Timeout指定测试方法执行超时时间性能验证、防止死循环三、断言方法(Assertions)JUnit 5 提供多种断言来验证测试结果。方法用途assertEquals(expected, actual)断言期望值与实际值相等assertTrue(condition) / assertFalse(condition)断言条件为真 / 假assertThrows(ExceptionType.class, executable)断言执行代码会抛出指定异常assertAll(executables...)组合多个断言,所有断言都会执行再报告失败(避免因一个失败而中断后续验证)四、参数化测试数据源通过 @ParameterizedTest 结合数据源注解,用多组数据测试同一逻辑。注解说明@ValueSource提供简单值数组(如 ints = {1, 2, 3})@CsvSource提供 CSV 格式的字符串数据,支持多列参数@MethodSource引用一个返回 Stream / Iterable 的静态方法提供测试数据@EnumSource使用枚举类的所有值或指定子集作为测试数据五、测试编写原则与最佳实践5.1 好的测试实践(推荐)✅ 示例代码@Test @DisplayName("当输入两个正数时应该返回正确的和") void shouldReturnSumWhenAddingTwoPositiveNumbers() { // Arrange - 准备测试数据 Calculator calculator = new Calculator(); // Act - 执行被测试的方法 int result = calculator.add(2, 3); // Assert - 验证结果 assertEquals(5, result); }✅ 核心原则AAA 模式:Arrange(准备) → Act(执行) → Assert(断言)命名清晰:测试方法名应具有描述性,表达“测什么”和“预期结果”使用 @DisplayName:进一步提高测试报告可读性单一职责:每个测试方法只验证一个功能点覆盖边界条件与异常:不仅要测试正常路径,还要测试空值、零值、负数、越界等异常场景5.2 应该避免的实践(反模式)❌ 反面示例@Test void test1() { // 测试多个不相关的功能 assertEquals(5, calculator.add(2, 3)); assertEquals(6, calculator.multiply(2, 3)); assertEquals("hello", stringUtils.reverse("olleh")); }❌ 错误做法测试方法名无意义(如 test1、testAdd)一个测试方法中验证多个不相关的功能点测试之间存在执行顺序或数据依赖忽略边界条件与异常分支测试代码逻辑过于复杂或包含大量重复代码六、常用测试类型类型说明基本功能测试验证方法在正常输入下的预期行为边界值测试测试输入为 null、空字符串、0、最大值、最小值等边界情况异常测试使用 assertThrows 验证错误输入时是否正确抛出异常参数化测试使用多组数据驱动同一测试逻辑,减少重复代码性能测试结合 @Timeout 或性能测试框架验证执行效率七、测试编写注意事项测试独立性:每个测试应能单独运行,不依赖其他测试的执行结果或执行顺序。数据自包含:测试数据应在测试方法内部准备,避免使用共享可变全局状态。断言消息:为关键断言添加描述性消息,便于快速定位失败原因。资源清理:在 @AfterEach 或 @AfterAll 中释放资源(如文件流、数据库连接)。代码简洁:保持测试代码易读、易维护,必要时可提取私有辅助方法。八、测试覆盖率目标指标建议值整体代码覆盖率≥ 80%核心业务逻辑重点覆盖,尽量达到 90% 以上公共方法所有 public 方法均应有对应测试异常分支与边界条件必须包含相关测试用例覆盖率工具使用 JaCoCo 等工具生成报告并持续监控笔记总结完毕。 按照以上结构和内容复习,即可系统掌握 JUnit 5 的使用方法与单元测试最佳实践。
2026年04月10日
5 阅读
0 评论
0 点赞
2026-04-09
并发编程基础概念
一、并发编程基础概念1.1 并发与并行并发:多任务交替执行(单核/多核),依赖 CPU 时间片轮转。宏观上是同时进行的,微观上是串行。并行:多任务同时执行(必须多核),利用多个 CPU 核心同时处理不同任务。1.2 Java 内存模型 (JMM)主内存:所有线程共享,存储实例变量、静态变量。工作内存:每个线程私有,存储主内存变量的副本。线程不能直接操作主内存,必须先读入工作内存,操作完成后再写回。JMM 定义的 8 种内存交互操作(原子指令):lock(锁定):作用于主内存,将变量标识为线程独占。unlock(解锁):作用于主内存,释放独占状态。read(读取):作用于主内存,将变量值传输到工作内存。load(载入):作用于工作内存,将 read 的值放入变量副本。use(使用):作用于工作内存,将变量值传递给执行引擎。assign(赋值):作用于工作内存,将执行引擎的值赋给变量副本。store(存储):作用于工作内存,将变量值传送到主内存。write(写入):作用于主内存,将 store 的值写入主内存变量。1.3 线程安全问题的根源与表现根源:主内存与工作内存数据不一致。竞态条件:结果依赖线程执行的相对时序(如 check-then-act)。数据竞争:多线程访问同一内存,至少一个写操作,且无同步。死锁:两个及以上线程互相持有对方需要的锁,永久阻塞。二、并发编程三大特性(详细版)特性详细描述实现手段可见性线程 A 修改变量后,线程 B 能立即看到变化。volatile、synchronized、Lock(锁释放前刷新主存)、final原子性一个或多个操作要么全执行,要么全不执行,中间不被调度中断。synchronized、Lock、java.util.concurrent.atomic 原子类有序性避免指令重排序带来的逻辑错误。单线程内语义表现为串行。volatile(内存屏障)、synchronized(锁释放与获取原则)三、线程基础 API 详解3.1 创建线程的方式(含代码细节)方式一:继承 Thread 类public class ThreadExample extends Thread { @Override public void run() { System.out.println("Thread is running"); } public static void main(String[] args) { ThreadExample t = new ThreadExample(); t.start(); } }方式二:实现 Runnable 接口public class RunnableExample implements Runnable { @Override public void run() { System.out.println("Runnable task is running"); } public static void main(String[] args) { Thread t = new Thread(new RunnableExample()); t.start(); // Lambda 简化写法 new Thread(() -> System.out.println("Lambda style")).start(); } }方式三:实现 Callable 接口(有返回值、可抛异常)import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class CallableExample implements Callable<String> { @Override public String call() throws Exception { Thread.sleep(1000); return "Callable result"; } public static void main(String[] args) throws Exception { FutureTask<String> futureTask = new FutureTask<>(new CallableExample()); new Thread(futureTask).start(); // 阻塞获取结果 System.out.println(futureTask.get()); } }3.2 Thread 类构造方法与核心控制构造方法细节:Thread():默认线程名 "Thread-" + n。Thread(String name):指定线程名。Thread(Runnable target):指定任务。Thread(Runnable target, String name):指定任务与名称。线程优先级与守护线程:Thread t = new Thread(); t.setPriority(Thread.MAX_PRIORITY); // 10(最高) t.setPriority(Thread.NORM_PRIORITY); // 5(默认) t.setPriority(Thread.MIN_PRIORITY); // 1(最低) // 守护线程:JVM 退出时不管守护线程是否结束 Thread daemonT = new Thread(() -> { while (true) { /* 后台任务,如日志清理 */ } }); daemonT.setDaemon(true); // 必须在 start() 前设置 daemonT.start();3.3 线程中断机制(方法签名详解)void interrupt():设置中断标志位为 true。boolean isInterrupted():检查标志位,不清除。static boolean interrupted():检查当前线程标志位,并清除(重置为 false)。Thread t = new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { // 业务逻辑 try { Thread.sleep(1000); } catch (InterruptedException e) { // 捕获异常后标志位会被清除,需手动再次中断以退出循环 Thread.currentThread().interrupt(); } } });四、synchronized 锁机制深度解析4.1 锁的三种形态(代码示例)对象锁(锁当前实例 this):class SyncDemo { // 同步实例方法 public synchronized void method1() { /* 锁 this */ } // 同步代码块 public void method2() { synchronized (this) { /* 锁 this */ } } }类锁(锁 Class 对象):class SyncDemo { // 同步静态方法 public static synchronized void staticMethod1() { /* 锁 SyncDemo.class */ } public void staticMethod2() { synchronized (SyncDemo.class) { /* 锁类对象 */ } } }4.2 锁升级机制(Java 6+,不可逆)偏向锁:无竞争时,在对象头记录线程 ID,线程再次进入仅需检查 ID,无需 CAS。轻量级锁:交替执行时,通过 CAS 自旋尝试获取锁,不阻塞线程(消耗 CPU)。重量级锁:激烈竞争时,自旋失败升级为 OS 互斥量,线程挂起进入阻塞队列(开销大)。五、volatile 与内存屏障5.1 volatile 特性总结保证可见性:写 volatile 变量时强制刷新工作内存到主内存;读 volatile 变量时强制从主内存加载。禁止重排序:通过插入内存屏障防止 CPU/编译器对指令进行重排。不保证原子性:i++ 操作(读-改-写)依然不安全。5.2 内存屏障分类(JMM 规范)屏障类型作用示例场景LoadLoad屏障前读操作完成,才能执行屏障后读操作。读变量A; LoadLoad; 读变量B;LoadStore屏障前读操作完成,才能执行屏障后写操作。读变量A; LoadStore; 写变量B;StoreStore屏障前写操作完成并可见,才能执行屏障后写操作。写变量A; StoreStore; 写变量B;StoreLoad屏障前写操作完成并可见,才能执行屏障后读操作。开销最大,兼具其他三者功能。写变量A; StoreLoad; 读变量B;volatile 屏障插入策略:volatile 写:前插 StoreStore,后插 StoreLoad。volatile 读:后插 LoadLoad + LoadStore。六、Lock 接口与 ReentrantLock 详细 API6.1 Lock 接口方法清单方法详细描述void lock()阻塞获取锁,不可中断,死等。void lockInterruptibly()可响应中断的获取锁,等待时若线程被中断则抛 InterruptedException。boolean tryLock()非阻塞,获取到返回 true,否则立即返回 false。boolean tryLock(long time, TimeUnit unit)超时等待,超时返回 false;等待时可响应中断。void unlock()释放锁。必须在 finally 块中调用。Condition newCondition()返回绑定此锁的条件变量。6.2 ReentrantLock 使用模板class X { private final ReentrantLock lock = new ReentrantLock(); public void m() { lock.lock(); // block until condition holds try { // ... method body } finally { lock.unlock(); // 关键:防止死锁 } } }6.3 公平锁与非公平锁构造// 非公平锁(默认,吞吐量高) ReentrantLock unfairLock = new ReentrantLock(); ReentrantLock unfairLock2 = new ReentrantLock(false); // 公平锁(FIFO,避免线程饥饿但上下文切换多) ReentrantLock fairLock = new ReentrantLock(true);6.4 可中断与超时示例// 可中断锁示例 try { lock.lockInterruptibly(); // 临界区 } catch (InterruptedException e) { // 线程被中断,退出等待队列 } finally { lock.unlock(); } // 超时锁示例 if (lock.tryLock(2, TimeUnit.SECONDS)) { try { // 获取成功 } finally { lock.unlock(); } } else { // 执行备选方案 }七、读写锁与 StampedLock7.1 ReentrantReadWriteLock 特性读写互斥:读锁与写锁互斥。锁降级:持有写锁的线程可以获取读锁,然后释放写锁,降级为读锁。不支持锁升级:持有读锁时直接获取写锁会导致死锁,JVM 禁止此行为。ReadWriteLock rwLock = new ReentrantReadWriteLock(); Lock readLock = rwLock.readLock(); Lock writeLock = rwLock.writeLock(); // 缓存更新场景:锁降级示例 writeLock.lock(); try { // 1. 更新数据库/缓存 readLock.lock(); // 在释放写锁前获取读锁 } finally { writeLock.unlock(); // 降级为读锁 } try { // 2. 后续读取操作(保证数据可见性) } finally { readLock.unlock(); }7.2 StampedLock 三种模式(Java 8+,不可重入)写锁:long stamp = stampedLock.writeLock(); 返回邮戳。悲观读锁:long stamp = stampedLock.readLock();乐观读:无锁机制,依赖版本校验。class Point { private double x, y; private final StampedLock sl = new StampedLock(); // 乐观读(读多写少性能最优) double distanceFromOrigin() { long stamp = sl.tryOptimisticRead(); // 获取乐观读邮戳 double currentX = x, currentY = y; // 无锁读取 if (!sl.validate(stamp)) { // 校验邮戳是否失效 stamp = sl.readLock(); // 失效则升级为悲观读 try { currentX = x; currentY = y; } finally { sl.unlockRead(stamp); } } return Math.sqrt(currentX * currentX + currentY * currentY); } }八、Condition 条件变量(替换 Object Monitor)8.1 核心方法对比Condition 方法Object 对应方法说明await()wait()释放锁并等待。必须在 while 循环中调用以防虚假唤醒。signal()notify()唤醒一个等待线程。signalAll()notifyAll()唤醒所有等待线程。awaitUninterruptibly()无不可中断等待。awaitUntil(Date deadline)wait(long timeout)带截止时间等待。8.2 生产者-消费者模型(多条件队列)class BoundedBuffer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); // 防御性编程:必须用 while 循环 public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) // 防止虚假唤醒 notFull.await(); // ... 入队逻辑 notEmpty.signal(); } finally { lock.unlock(); } } }九、原子类与 CAS 底层原理9.1 AtomicInteger 常用方法方法签名操作描述int get()获取当前值。void set(int newValue)设置新值(无原子性保证,仅可见性)。int getAndSet(int newValue)设置新值,返回旧值。boolean compareAndSet(expect, update)CAS 核心操作。int getAndIncrement()等价于 i++。int incrementAndGet()等价于 ++i。int getAndAdd(int delta)加 delta 并返回旧值。9.2 ABA 问题与解决方案ABA 问题描述:线程 1 读取值 A,线程 2 改为 B,线程 3 又改回 A。线程 1 使用 CAS 检查时发现值仍为 A,误以为未变化而更新成功。解决方案:AtomicStampedReference 引入版本号。// 泛型类:AtomicStampedReference<V> AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0); // 初始值100,版本号0 int stamp = ref.getStamp(); int oldValue = ref.getReference(); // 更新时需同时匹配值和时间戳 boolean success = ref.compareAndSet(oldValue, 200, stamp, stamp + 1);
2026年04月09日
8 阅读
0 评论
0 点赞
1
2
3
4