IoC容器
Spring
为我们提供了一个容器,用于管理所有的JavaBean组件,这是Spring框架最核心的概念
举个例子来说明IoC容器到底是啥:
假设现在有一个MemberService
和BookService
,现在它俩都需要操作数据库,用传统方式自然是在每个Service中都创建数据源实例,比如:
public class MemberService {
private HikariConfig config = new HikariConfig();
private DataSource dataSource = new HikariDataSource(config);
}
public class BookService {
private HikariConfig config = new HikariConfig();
private DataSource dataSource = new HikariDataSource(config);
}
在每个Service中都需要重复创建这些对象,随着Service
越来越多,难道我们要一个个手动创建出来吗?完全可以共享同一个DataSource
,那IoC就是用来解决这些问题的,在Ioc模式下控制权发生了反转,所有的组件都由容器负责,而不是我们自己手动创建,最简单的方式就是通过XML文件来实现:
在这个文件中创建了三个JavaBean组件,可以发现两个Service
共享同一个数据源ref="dataSource"
,创建好Bean之后就需要使用了Bean了
依赖注入方式:
注入在IoC容器中管理的Bean,可通过set()
或构造方法实现
public class BookService {
private DataSource dataSource;
public setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
}
创建Spring项目
通过maven创建即可,需引入spring-context
依赖
4.0.0
com.itranswarp.learnjava
spring-ioc-appcontext
1.0-SNAPSHOT
jar
UTF-8
UTF-8
11
11
11
5.2.3.RELEASE
org.springframework
spring-context
${spring.version}
一个特定的application.xml
文件,就是之前组装Bean的文件
最后得告诉容器为我们创建并装配好所有得Bean,在主启动类中添加以下代码
public static void main(String[] args) {
// 在resources目录下加载配置文件,并完成Bean的创建
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
// 获取Bean
MemberService member = context.getBean(MemberService.class);
// 调用方法
member.login("username","password");
}
使用注解配置
其实完全可以不用XML文件配置Bean,使用注解配置更加简单
直接在类上添加@Component
注解:
@Component
public class MailService {
}
@Component
public class UserService {
@Autowired
private MailService mailService;
}
@Component
就等于在容器中定义了一个Bean,默认名为首字母小写,@Autowired
就等于使用set()
进行依赖注入
由于没有了配置文件,所以主启动类中的加载方式也有了变化
@Configuration
@ComponentScan
public class AppConfig {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
}
}
@Configuration
表示它是一个配置类,等于application.xml
,@ComponentScan
用于扫描当前类以及所在子包所有标注了@Component
的类并将其创建,一定要严格按照这个包结构来创建类
Scope
Spring容器创建的Bean默认都是单例的,所以说通过context.getBean()
获取的Bean都是同一个实例,我们也可以让它每次都返回一个新的实例,把这种Bean称为原型,在类上下面的注解即可
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
注入List
通过定义接口和实现类,将所有该类型的Bean都注入到一个List中
public interface Validator {
}
@Component
public class NameValidator implements Validator {
}
@Component
public class PasswordValidator implements Validator {
}
@Component
public class Validators {
@Autowired
List validators;
}
Validator
接口有两个实现类,在Validators
中定义好了集合的泛型,通过@Autowired
就可将所有Validator
类型的Bean注入到一个List中
第三方Bean
用于一个Bean不在我们的包管理之内
@Configuration
@ComponentScan
public class AppConfig {
// 创建一个Bean:
@Bean
ZoneId createZoneId() {
return ZoneId.of("Z");
}
}
@Bean
只调用一次,它返回的Bean也是单例的
初始化和销毁
用于一个Bean在被注入后进行初始化操作以及容器关闭时进行销毁操作,需引入一个特定依赖
javax.annotation
javax.annotation-api
1.3.2
@Component
public class MailService {
@PostConstruct
public void init() {
System.out.println("init");
}
@PreDestroy
public void shutdown() {
System.out.println("shutdown");
}
}
在MailService
被注入后就会执行init()
,在容器关闭时执行shutdown()
,需调用close()
Resource
Spring提供了一个org.springframework.core.io.Resource
用于读取配置文件
@Value("classpath:/logo.txt")
private Resource resource;
还有更简单的方式,使用注解:
@PropertySource("app.properties") // 表示读取classpath的app.properties
public class AppConfig {
@Value("${app.zone:Z}")
String zoneId;
}
${app.zone:Z}
表示如果Key不存在就用默认值Z
条件装配
当满足特定条件创建Bean,使用@Conditional
,看一个简单例子:
@ConditionalOnProperty(name="app.smtp", havingValue="true")
public class MailService {
}
如果配置文件中有app.smtp
并且值为true才创建
AOP
面向切面编程,可以将常用的比如日志,事务等从每个业务方法中抽离出来,本质其实是一个动态代理
引入AOP依赖:
org.springframework
spring-aspects
5.2.3.RELEASE
@Aspect
@Component
public class LoggingAspect {
// 在执行UserService的每个方法前执行:
@Before("execution(public * com.zouyc.learn.service.UserService.*(..))")
public void doAccessCheck() {
System.err.println("[Before] do access check...");
}
// 在执行MailService的每个方法前后执行:
@Around("execution(public * com.zouyc.learn.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;
}
}
通过注解加特定的语法实现在方法执行前后做些事情,pjp.proceed()
执行MailService
的方法,最后还需在配置类上开启@EnableAspectJAutoProxy
可以看到AspectJ
的语法是非常复杂的,怎样更简洁呢?使用纯注解
自定义一个注解
@Target(METHOD)
@Retention(RUNTIME)
public @interface MetricTime {
String value();
}
在需要被监控的方法上添加注解
@MetricTime("register")
public void register() {
System.out.println("registration success");
}
定义Aspect
类
@Aspect
@Component
public class MetricAspect {
@Around("@annotation(metricTime)")
public Object metric(ProceedingJoinPoint joinPoint, MetricTime metricTime) throws Throwable {
return joinPoint.proceed();
}
}
@Around("@annotation(metricTime)")
找到标注了@MetricTime
的方法,需注意方法参数上的metricTime
和@annotation(metricTime)
必须一样
AOP避坑:
始终使用get()
方法访问,而不直接访问字段
public String sendMail() {
ZoneId zoneId = userService.zoneId;
System.out.println(zoneId); // null
}
上述代码会报空指针异常,为什么?
原因在于成员变量的初始化,正常来说构造方法第一行总是调用super()
,但是Spring通过CGLIB动态创建的代理类并未调用super()
,因此从父类继承的成员变量以及自身的成员变量都没有初始化,如何解决?
public String sendMail() {
// 不要直接访问UserService的字段:
ZoneId zoneId = userService.getZoneId();
}
为什么调用getZoneId()
就能解决呢?因为代理类会覆写getZoneId()
,并将其交给原始实例,这样变量就得到了初始化,就不会报空指针异常了,最后一点,如果你的类有可能被代理,就不要编写public final
方法,因为无法被覆写
访问数据库
Spring提供了一个JdbcTemplate
让我们操作JDBC,所以我们只需实例化一个JdbcTemplate
基本使用方法:
创建配置文件jdbc.properties
,这里使用的是HSQLDB,它可以以内存模式运行,适合测试
com.zaxxer
HikariCP
3.4.2
org.hsqldb
hsqldb
2.5.0
# 数据库文件名为testdb:
jdbc.url=jdbc:hsqldb:file:testdb
# Hsqldb默认的用户名是sa,口令是空字符串:
jdbc.username=sa
jdbc.password=
读取数据库配置文件并创建DataSource
和JdbcTemplate
@PropertySource("classpath:jdbc.properties")
public class AppConfig {
@Value("${jdbc.url}")
private String jdbcUrl;
@Value("${jdbc.username}")
private String jdbcUsername;
@Value("${jdbc.password}")
private String jdbcPassword;
@Bean
DataSource createDataSource() {
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setJdbcUrl(jdbcUrl);
hikariConfig.setUsername(jdbcUsername);
hikariConfig.setPassword(jdbcPassword);
hikariConfig.addDataSourceProperty("autoCommit", "true");
hikariConfig.addDataSourceProperty("connectionTimeout", "5");
hikariConfig.addDataSourceProperty("idleTimeout", "60");
return new HikariDataSource(hikariConfig);
}
@Bean
JdbcTemplate createJdbcTemplate(@Autowired DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
由于是在HSQLDB内存模式下工作的,所以我们还需要建立对应的表
@Autowired
JdbcTemplate jdbcTemplate;
@PostConstruct
public void init() {
jdbcTemplate.update("CREATE TABLE IF NOT EXISTS users (" //
+ "id BIGINT IDENTITY NOT NULL PRIMARY KEY, " //
+ "email VARCHAR(100) NOT NULL, " //
+ "password VARCHAR(100) NOT NULL, " //
+ "name VARCHAR(100) NOT NULL, " //
+ "UNIQUE (email))");
}
在其他Service中注入JdbcTemplate
即可
JdbcTemplate
的用法:
T execute(ConnectionCallback
:使用Jdbc的Connection
T execute(String sql, PreparedStatementCallback
:使用Jdbc的PreparedStatement
T queryForObject(String sql, @Nullable Object[] args, RowMapper
:RowMapper将ResultSet映射成一个JavaBean并返回,返回一行记录
List
:返回多行记录
INSERT
操作:
因为INSERT
涉及到主键自增, 所以它比较特殊
public User register(String email, String password, String name) {
// 创建一个KeyHolder:
KeyHolder holder = new GeneratedKeyHolder();
if (1 != jdbcTemplate.update(
// 参数1:PreparedStatementCreator
(conn) -> {
// 创建PreparedStatement时,必须指定RETURN_GENERATED_KEYS:
var ps = conn.prepareStatement("INSERT INTO users(email,password,name) VALUES(?,?,?)",
Statement.RETURN_GENERATED_KEYS);
ps.setObject(1, email);
ps.setObject(2, password);
ps.setObject(3, name);
return ps;
},
// 参数2:KeyHolder
holder)
) {
throw new RuntimeException("Insert failed.");
}
// 从KeyHolder中获取返回的自增值:
return new User(holder.getKey().longValue(), email, password, name);
}
声明式事务
Spring提供了一个PlatformTransactionManager
来表示事务管理器,事务由TransactionStatus
表示
PlatformTransactionManager transactionManager = new DataSourceTransactionManager();
TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
使用声明式事务:
@EnableTransactionManagement
public class AppConfig {
@Bean
PlatformTransactionManager createTxManager(@Autowired DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
在需要使用的方法上添加@Transactional
,或者在类上添加(表示类中所有public方法都支持事务),事务的原理仍是AOP代理,所以开启事务之后就不必添加EnableAspectJAutoProxy
,判断事务回滚,只需抛出RuntimeException
,如果要针对某个异常回滚,就在注解上定义出来:
@Transactional(rollbackFor = {RuntimeException.class, IOException.class})
事务的传播:
默认级别为REQUIRED
,看代码:
@Transactional
public User register(String email, String password, String name) {
// 插入用户记录:
User user = jdbcTemplate.insert("...");
// 增加100积分:
bonusService.addBonus(user.id, 100);
}
可以看到上述代码中register()
开启了事务,但是在方法中又调用了另一个bonusService
,bonusService
没必要再创建新事务,事务的默认传播级别REQUIRED
表示,如果当前有事务就自动加入当前事务,如果没有才创建新事务,所以register()
方法的开始和结束就是整个事务的范围,当然除了REQUIRED
还有其他传播级别,这里就不一一赘述了
DAO层
DAO即Data Access Object的缩写,就是专门用来和数据库打交道的层级,负责处理各种业务逻辑
Spring提供了JdbcDaoSupport
简化数据库操作,它的核心就是持有一个JdbcTemplate
public abstract class JdbcDaoSupport extends DaoSupport {
@Nullable
private JdbcTemplate jdbcTemplate;
/**
* Set the JdbcTemplate for this DAO explicitly,
* as an alternative to specifying a DataSource.
*/
public final void setJdbcTemplate(@Nullable JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
initTemplateConfig();
}
/**
* Return the JdbcTemplate for this DAO,
* pre-initialized with the DataSource or set explicitly.
*/
@Nullable
public final JdbcTemplate getJdbcTemplate() {
return this.jdbcTemplate;
}
}
可以看到JdbcDaoSupport
并没有自动注入JdbcTemplate
,所以得自己注入
编写一个AbstractDao
用于注入JdbcTemplate
public abstract class AbstractDao extends JdbcDaoSupport {
@Autowired
private JdbcTemplate jdbcTemplate;
@PostConstruct
public void init() {
super.setJdbcTemplate(jdbcTemplate);
}
}
这样继承了AbstractDao
的子类就可以直接调用getJdbcTemplate()
获取JdbcTemplate
我们还可以把更多的常用方法都写到AbstractDao
中,这里就需要用到泛型
public abstract class AbstractDao extends JdbcDaoSupport {
private String table;
private Class entityClass;
private RowMapper rowMapper;
@Autowired
private JdbcTemplate jdbcTemplate;
@PostConstruct
public void init() {
super.setJdbcTemplate(jdbcTemplate);
}
public AbstractDao() {
this.entityClass = getParameterizedType();
this.table = this.entityClass.getSimpleName().toLowerCase() + "s";
this.rowMapper = new BeanPropertyRowMapper<>(entityClass);
}
public Class getParameterizedType() {
Type type = this.getClass().getGenericSuperclass();
if (type instanceof ParameterizedType) {
ParameterizedType t = (ParameterizedType) type;
Type[] types = t.getActualTypeArguments();
Type firstType = types[0];
return (Class) firstType;
}
return null;
}
public T getById(long id) {
}
public List getAll(int pageIndex) {
}
public void deleteById(long id) {
}
public Class getEntityClass() {
return entityClass;
}
public String getTable() {
return table;
}
}
集成MyBatis
MyBatis是一款半自动ORM框架,只负责把ResultSet映射成JavaBean,SQL仍需自己编写
要使用它先要引入依赖:
org.mybatis
mybatis
3.5.4
org.mybatis
mybatis-spring
2.0.4
使用MyBatis的核心就是创建SqlSessionFactory
@Bean
SqlSessionFactoryBean createSqlSessionFactoryBean(@Autowired DataSource dataSource) {
var sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean;
}
它可以直接使用声明式事务,创建事务管理和使用JDBC是一样的
定义Mapper接口,编写SQL语句:
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User getById(@Param("id") long id);
}
扫描并创建Mapper的实现类:
@MapperScan("com.zouyc.learn.mapper")
public class AppConfig {
}
在业务逻辑中直接注入UserMapper
即可
XML配置:为了更加灵活的编写SQL语句,就需要使用XML文件来做到,详情查看官方文档