廖雪峰spring-1 spring 开发 IOC容器 IOC原理
依赖注入方式
无侵入容器
装配Bean 1 2 3 xml方式 IOC容器可使用ApplicationContext、BeanFactory
使用Annotation配置 1 2 3 4 5 @Component @Autowired 可以写在构造方法、字段、方法里 @Configuration @ComponentScan
定制Bean scope 1 2 @Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
注入List 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Component public class Validators { @Autowired List<Validator> validators; public void validate(String email, String password, String name) { for (var validator : this.validators) { validator.validate(email, password, name); } } } @Component @Order(1) ... @Autowired(required = false)
创建第三方Bean 1 2 3 4 5 6 7 8 9 @Configuration @ComponentScan public class AppConfig { // 创建一个Bean: @Bean ZoneId createZoneId() { return ZoneId.of("Z"); } }
可选注入 1 2 3 4 @Component public class MailService { @Autowired(required = false) ZoneId zoneId = ZoneId.systemDefault();
初始化和销毁 1 2 3 4 5 6 7 8 9 10 @PostConstruct public void init() { System.out.println("Init mail service with zoneId = " + this.zoneId); } @PreDestroy public void shutdown() { System.out.println("Shutdown mail service"); } }
使用别名 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Configuration @ComponentScan public class AppConfig { @Bean("z") ZoneId createZoneOfZ() { return ZoneId.of("Z"); } @Bean @Qualifier("utc8") ZoneId createZoneOfUTC8() { return ZoneId.of("UTC+08:00"); } } @Component public class MailService { @Autowired(required = false) @Qualifier("z") // 指定注入名称为"z"的ZoneId ZoneId zoneId = ZoneId.systemDefault(); ... }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Configuration @ComponentScan public class AppConfig { @Bean @Primary // 指定为主要Bean @Qualifier("z") ZoneId createZoneOfZ() { return ZoneId.of("Z"); } @Bean @Qualifier("utc8") ZoneId createZoneOfUTC8() { return ZoneId.of("UTC+08:00"); } }
使用FactoryBean 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 由于可以用@Bean方法创建第三方Bean,本质上@Bean方法就是工厂方法,所以,FactoryBean已经用得越来越少了。 @Component public class ZoneIdFactoryBean implements FactoryBean<ZoneId> { String zone = "Z"; @Override public ZoneId getObject() throws Exception { return ZoneId.of(zone); } @Override public Class<?> getObjectType() { return ZoneId.class; } }
使用Resource 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Component public class AppService { @Value("classpath:/logo.txt") private Resource resource; private String logo; @PostConstruct public void init() throws IOException { try (var reader = new BufferedReader( new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) { this.logo = reader.lines().collect(Collectors.joining("\n")); } } }
注入配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Configuration @ComponentScan @PropertySource("app.properties") // 表示读取classpath的app.properties public class AppConfig { @Value("${app.zone:Z}") String zoneId; @Bean ZoneId createZoneId() { return ZoneId.of(zoneId); } } 另一种注入配置的方式是先通过一个简单的JavaBean持有所有的配置
使用条件装配 @Profile 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Configuration @ComponentScan public class AppConfig { @Bean @Profile("!test") ZoneId createZoneId() { return ZoneId.systemDefault(); } @Bean @Profile("test") ZoneId createZoneIdForTest() { return ZoneId.of("America/New_York"); } }
@Conditional 1 2 3 4 5 6 7 8 9 10 11 @Component @Conditional(OnSmtpEnvCondition.class) public class SmtpMailService implements MailService { ... } public class OnSmtpEnvCondition implements Condition { public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { return "true".equalsIgnoreCase(System.getenv("smtp")); } }
@ConditionalOnProperty 1 2 3 4 5 @Component @ConditionalOnProperty(name="app.smtp", havingValue="true") public class MailService { ... }
1 2 3 4 5 @Component @ConditionalOnProperty(name = "app.storage", havingValue = "file", matchIfMissing = true) public class FileUploader implements Uploader { ... }
1 2 3 4 5 @Component @ConditionalOnProperty(name = "app.storage", havingValue = "s3") public class S3Uploader implements Uploader { ... }
1 2 3 4 5 @Component public class UserImageService { @Autowired Uploader uploader; }
@ConditionalOnClass 1 2 3 4 5 @Component @ConditionalOnClass(name = "javax.mail.Transport") public class MailService { ... }
使用AOP AOP原理 1 2 3 4 5 6 7 在Java平台上,对于AOP的织入,有3种方式: 编译期:在编译时,由编译器把切面调用编译进字节码,这种方式需要定义新的关键字并扩展编译器,AspectJ就扩展了Java编译器,使用关键字aspect来实现织入; 类加载器:在目标类被装载到JVM时,通过一个特殊的类加载器,对目标类的字节码重新“增强”; 运行期:目标对象和切面都是普通Java类,通过JVM的动态代理功能或者第三方库实现运行期动态织入。 最简单的方式是第三种,Spring的AOP实现就是基于JVM的动态代理
装配AOP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Aspect @Component public class LoggingAspect { // 在执行UserService的每个方法前执行: @Before("execution(public * com.itranswarp.learnjava.service.UserService.*(..))") public void doAccessCheck() { System.err.println("[Before] do access check..."); } // 在执行MailService的每个方法前后执行: @Around("execution(public * com.itranswarp.learnjava.service.MailService.*(..))") public Object doLogging(ProceedingJoinPoint pjp) throws Throwable { System.err.println("[Around] start " + pjp.getSignature()); Object retVal = pjp.proceed(); System.err.println("[Around] done " + pjp.getSignature()); return retVal; } }
1 2 3 4 5 6 @Configuration @ComponentScan @EnableAspectJAutoProxy public class AppConfig { ... }
拦截器类型 1 2 3 4 5 6 7 8 9 10 11 顾名思义,拦截器有以下类型: @Before:这种拦截器先执行拦截代码,再执行目标代码。如果拦截器抛异常,那么目标代码就不执行了; @After:这种拦截器先执行目标代码,再执行拦截器代码。无论目标代码是否抛异常,拦截器代码都会执行; @AfterReturning:和@After不同的是,只有当目标代码正常返回时,才执行拦截器代码; @AfterThrowing:和@After不同的是,只有当目标代码抛出了异常时,才执行拦截器代码; @Around:能完全控制目标代码是否执行,并可以在执行前后、抛异常后执行任意拦截代码,可以说是包含了上面所有功能。
使用注解装配AOP AOP避坑指南 访问数据库 使用JDBC 使用声明式事务 回滚事务 事务边界 事务传播 使用DAO 集成Hibernate 集成JPA 集成MyBatis 设计ORM 开发Web应用 使用Spring MVC
配置Spring MVC
1 2 3 4 5 6 7 8 9 @Bean WebMvcConfigurer createWebMvcConfigurer() { return new WebMvcConfigurer() { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**").addResourceLocations("/static/"); } }; }
1 2 3 4 5 6 7 8 9 10 11 12 @Bean ViewResolver createViewResolver(@Autowired ServletContext servletContext) { var engine = new PebbleEngine.Builder().autoEscaping(true) // cache: .cacheActive(false) // loader: .loader(new Servlet5Loader(servletContext)) .build(); var viewResolver = new PebbleViewResolver(engine); viewResolver.setPrefix("/WEB-INF/templates/"); viewResolver.setSuffix(""); retur
编写Controller
使用REST
注意servlet和spring mvc使用filter的区别
@Component public class AuthFilter implements Filter {
@Order(1) @Component public class LoggerInterceptor implements HandlerInterceptor {
1 2 3 4 5 6 7 8 9 ##### 处理异常 #### 处理CORS #### 国际化
@Primary @Bean LocaleResolver createLocaleResolver() { var clr = new CookieLocaleResolver(); clr.setDefaultLocale(Locale.ENGLISH); clr.setDefaultTimeZone(TimeZone.getDefault()); return clr; }
1 2 3 4 5 6 7 ##### 提取资源文件 ##### 创建MessageSource ##### 实现多语言
private Extension createExtension(MessageSource messageSource) { return new AbstractExtension() { @Override public Map<String, Function> getFunctions() { return Map.of(“_”, new Function() { @Override public List getArgumentNames() { return null; }
@Override
public Object execute(Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
String key = (String) args.get("0");
List<Object> arguments = this.extractArguments(args);
Locale locale = (Locale) context.getVariable("__locale__");
return messageSource.getMessage(key, arguments.toArray(), "???" + key + "???", locale);
}
private List<Object> extractArguments(Map<String, Object> args) {
int i = 1;
List<Object> arguments = new ArrayList<>();
while (args.containsKey(String.valueOf(i))) {
Object param = args.get(String.valueOf(i));
arguments.add(param);
i++;
}
return arguments;
}
});
}
};
}
这段代码是Java中的匿名内部类(Anonymous Inner Class)的使用,配合接口回调、覆盖方法(Override)和Lambda表达式之前的做法来实现某些功能。我会逐步解释这段代码:
匿名内部类: 在Java中,如果你想实现一个接口或扩展一个类但不希望为它定义一个名称,你可以使用匿名内部类。这通常在你只需要使用一次该类的实例时很有用。 在这段代码中,AbstractExtension被匿名地扩展了,并且重写了其getFunctions方法。 2. 接口回调: AbstractExtension中的getFunctions方法返回一个Map<String, Function>。这里,它返回了一个只有一个条目的映射,其中键是”_”,值是一个实现了Function接口的匿名内部类。 3. 方法覆盖: 在Function的匿名内部类中,getArgumentNames和execute方法被覆盖了。
getArgumentNames返回null,意味着此函数不接受任何命名参数。
execute方法定义了当此函数被调用时应执行的操作。
execute方法的功能: 这个方法从传入的参数中提取一个键(key)和一组参数(arguments)。然后,它从EvaluationContext中获取Locale对象,并使用messageSource的getMessage方法从消息源中获取消息。
key:用于从消息源中查找消息。
arguments:用于替换消息中的占位符。
"???" + key + "???":当messageSource找不到给定的key时,返回的默认消息。
locale:确定消息的本地化版本。
extractArguments方法: 这是一个私有方法,用于从传入的args映射中提取参数。它从键1开始,检查是否有对应的值,如果有,则将其添加到arguments列表中,并递增索引。
总结: 这段代码定义了一个扩展AbstractExtension的匿名类,并为其提供了一个名为”_”的函数。这个函数的功能是从消息源中根据给定的键和参数获取一个本地化消息。这是模板引擎(如Pebble)中常见的做法,允许开发者在模板中调用自定义函数。
虽然现代的Java版本(如Java 8及以上)提供了Lambda表达式,这使得上述代码可以更加简洁和易读,但此代码展示了在较早的Java版本中如何实现类似的功能。
1 2 3 4 5 6 7 ##### 切换Locale #### 异步处理 ##### 第一种async处理方式是返回一个`Callable`
@RestController @RequestMapping(“/api”) public class ApiController { @GetMapping(“/users”) public Callable<List> users() { logger.info(“get users…”); return () -> { try { Thread.sleep(3000); } catch (InterruptedException e) { } logger.info(“return users…”); return userService.getUsers(); }; }
这段代码展示了Java中的Callable接口和@GetMapping注解的用法,通常在Spring框架中用于构建Web应用。我会分步解释这段代码:
@GetMapping(“/users”): 这是Spring Web框架中的一个注解,用于将一个HTTP GET请求映射到users()方法。当用户或客户端向/users这个URL发送GET请求时,Spring会调用users()方法。
public Callable<List> users(): 这是一个公共方法,它返回一个Callable<List>对象。Callable是Java的并发包中的一个接口,它表示一个可能抛出异常的值返回任务。与Runnable不同,Callable的call()方法有一个返回值。
return () -> {…};: 这是一个Lambda表达式,它创建并返回了一个实现了Callable接口的匿名类的实例。这个Lambda表达式定义了一个无参数的call()方法。
Thread.sleep(3000);: 在Lambda表达式的call()方法内部,程序会暂停执行3秒(3000毫秒)。这通常用于模拟长时间运行的任务或数据库查询,以便测试异步处理或响应超时等情况。
return userService.getUsers();: 当线程休眠结束后,call()方法会调用userService.getUsers()方法,并返回结果。这里假设userService是一个提供用户列表的服务类。
总结: 这段代码的主要目的是异步处理GET请求。当用户向/users发送请求时,Spring不会直接调用userService.getUsers()方法并等待结果,而是会返回一个Callable对象。这个Callable对象会在一个单独的线程中执行(虽然在这段代码中并没有明确显示异步执行的部分,但在Spring中,Callable通常与异步执行结合使用),并且在执行过程中模拟了一个3秒的延迟。这意味着,如果客户端发送了请求,它不会立即得到响应,而是会在大约3秒后得到用户列表。
这种异步处理的方式可以提高应用的响应能力和吞吐量,尤其是在处理长时间运行的任务时。不过,请注意,为了真正利用异步处理的优势,你还需要确保Spring配置为支持异步方法执行(通常通过@EnableAsync注解和@Async注解来实现)。
如果不使用Lambda表达式,可以使用匿名内部类的方式来实现类似的功能。在Spring框架中,Callable通常与@Async注解一起使用来实现异步执行。对于Callable<List> users()方法,如果不使用Lambda表达式,可以重写为:
@GetMapping(“/users”) @Async public Callable<List> users() { return new Callable<List>() { @Override public List call() throws Exception { // 模拟3秒耗时: Thread.sleep(3000); return userService.getUsers(); } }; }
这里,我们定义了一个匿名内部类,它实现了Callable<List>接口,并覆盖了call()方法。在call()方法中,我们模拟了一个3秒的延迟,然后调用userService.getUsers()方法来获取用户列表。
@Component public class UserService {
final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
JdbcTemplate jdbcTemplate;
RowMapper<User> userRowMapper = new BeanPropertyRowMapper<>(User.class);
public List<User> getUsers() {
return jdbcTemplate.query("SELECT * FROM users", userRowMapper);
}
这是Java中Spring框架的JdbcTemplate用法,具体来说是用于从数据库中查询数据并自动映射到Java对象的用法。
解释如下:
RowMapper userRowMapper = new BeanPropertyRowMapper<>(User.class); 这里创建了一个RowMapper的实例,用于将数据库查询结果的每一行映射到User类的实例。BeanPropertyRowMapper是Spring提供的一个便利的RowMapper实现,它使用JavaBean的属性名与数据库表的列名之间的匹配来自动完成映射。
这意味着,如果你的User类有一个名为id的属性,而数据库users表有一个名为id的列,那么BeanPropertyRowMapper会自动将这一列的值映射到User对象的id属性。 2. public List getUsers() { … }
这是一个公共方法,返回一个User对象的列表。 3. return jdbcTemplate.query(“SELECT * FROM users”, userRowMapper);
这里使用jdbcTemplate的query方法来执行SQL查询,并返回结果。查询的SQL是”SELECT * FROM users”,这意味着从users表中选择所有列的所有行。
userRowMapper作为第二个参数传递给query方法,它告诉jdbcTemplate如何将查询结果的每一行映射到User对象。
最终,query方法返回一个List,其中包含了从users表中查询到的所有行,每一行都被映射为一个User对象。
总之,这段代码使用Spring的JdbcTemplate来执行一个SQL查询,并将查询结果自动映射到Java对象。这是一个在Spring应用中常见的数据库访问模式,它简化了数据库操作的代码,并使得代码更加清晰和易于维护。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ##### 第二种async处理方式是返回一个DeferredResult对象 ##### 使用Filter #### 使用WebSocket ### 集成第三方组件 #### 集成JavaMail
ActiveMQ
1 2 3 4 5 #### 集成JMS #### 使用Scheduler
@Scheduled(initialDelay = 30_000, fixedDelayString = “${task.checkDiskSpace:30000}”)
1 2 3 4 5 ##### 集成Quartz #### 集成JMX
JMX是Java Management Extensions,它是一个Java平台的管理和监控接口。
MBean(Managed Bean)
MBeanServer
## spring boot开发
## spring cloud开发