IoC容器

Spring为咱们提供了一个容器,用于治理所有的JavaBean组件,这是Spring框架最外围的概念

举个例子来阐明IoC容器到底是啥:

假如当初有一个MemberServiceBookService,当初它俩都须要操作数据库,用传统形式天然是在每个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文件来实现:

<beans>    <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test" />        <property name="username" value="root" />        <property name="password" value="password" />        <property name="maximumPoolSize" value="10" />        <property name="autoCommit" value="true" />    </bean>    <bean id="bookService" class="BookService">        <property name="dataSource" ref="dataSource" />    </bean>    <bean id="memberService" class="MemberService">        <property name="dataSource" ref="dataSource" />    </bean></beans>

在这个文件中创立了三个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依赖

<project xmlns="http://maven.apache.org/POM/4.0.0"    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <groupId>com.itranswarp.learnjava</groupId>    <artifactId>spring-ioc-appcontext</artifactId>    <version>1.0-SNAPSHOT</version>    <packaging>jar</packaging>    <properties>        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>        <maven.compiler.source>11</maven.compiler.source>        <maven.compiler.target>11</maven.compiler.target>        <java.version>11</java.version>        <spring.version>5.2.3.RELEASE</spring.version>    </properties>    <dependencies>        <dependency>            <groupId>org.springframework</groupId>            <artifactId>spring-context</artifactId>            <version>${spring.version}</version>        </dependency>    </dependencies></project>

一个特定的application.xml文件,就是之前组装Bean的文件

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    xsi:schemaLocation="http://www.springframework.org/schema/beans        https://www.springframework.org/schema/beans/spring-beans.xsd"></beans>

最初得通知容器为咱们创立并拆卸好所有得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注解:

@Componentpublic class MailService {}
@Componentpublic class UserService {    @Autowired    private MailService mailService;}

@Component就等于在容器中定义了一个Bean,默认名为首字母小写,@Autowired就等于应用set()进行依赖注入

因为没有了配置文件,所以主启动类中的加载形式也有了变动

@Configuration@ComponentScanpublic 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 {}
@Componentpublic class NameValidator implements Validator {}
@Componentpublic class PasswordValidator implements Validator {}
@Componentpublic class Validators {    @Autowired    List<Validator> validators;}

Validator接口有两个实现类,在Validators中定义好了汇合的泛型,通过@Autowired就可将所有Validator类型的Bean注入到一个List中

第三方Bean

用于一个Bean不在咱们的包治理之内

@Configuration@ComponentScanpublic class AppConfig {    // 创立一个Bean:    @Bean    ZoneId createZoneId() {        return ZoneId.of("Z");    }}

@Bean只调用一次,它返回的Bean也是单例的

初始化和销毁

用于一个Bean在被注入后进行初始化操作以及容器敞开时进行销毁操作,需引入一个特定依赖

<dependency>    <groupId>javax.annotation</groupId>    <artifactId>javax.annotation-api</artifactId>    <version>1.3.2</version></dependency>
@Componentpublic 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.propertiespublic 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依赖:

<dependency>    <groupId>org.springframework</groupId>    <artifactId>spring-aspects</artifactId>    <version>5.2.3.RELEASE</version></dependency>
@Aspect@Componentpublic 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@Componentpublic 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,它能够以内存模式运行,适宜测试

<dependency>    <groupId>com.zaxxer</groupId>    <artifactId>HikariCP</artifactId>    <version>3.4.2</version></dependency><dependency>    <groupId>org.hsqldb</groupId>    <artifactId>hsqldb</artifactId>    <version>2.5.0</version></dependency>
# 数据库文件名为testdb:jdbc.url=jdbc:hsqldb:file:testdb# Hsqldb默认的用户名是sa,口令是空字符串:jdbc.username=sajdbc.password=

读取数据库配置文件并创立DataSourceJdbcTemplate

@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内存模式下工作的,所以咱们还须要建设对应的表

@AutowiredJdbcTemplate jdbcTemplate;@PostConstructpublic 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<T> action):应用Jdbc的Connection

T execute(String sql, PreparedStatementCallback<T> action):应用Jdbc的PreparedStatement

T queryForObject(String sql, @Nullable Object[] args, RowMapper<T> rowMapper):RowMapper将ResultSet映射成一个JavaBean并返回,返回一行记录

List<T> query(String sql, @Nullable Object[] args, RowMapper<T> rowMapper):返回多行记录

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());

应用申明式事务:

@EnableTransactionManagementpublic class AppConfig {    @Bean    PlatformTransactionManager createTxManager(@Autowired DataSource dataSource) {        return new DataSourceTransactionManager(dataSource);    }}

在须要应用的办法上增加@Transactional,或者在类上增加(示意类中所有public办法都反对事务),事务的原理仍是AOP代理,所以开启事务之后就不用增加EnableAspectJAutoProxy,判断事务回滚,只需抛出RuntimeException,如果要针对某个异样回滚,就在注解上定义进去:

@Transactional(rollbackFor = {RuntimeException.class, IOException.class})

事务的流传:

默认级别为REQUIRED,看代码:

@Transactionalpublic User register(String email, String password, String name) {    // 插入用户记录:    User user = jdbcTemplate.insert("...");    // 减少100积分:    bonusService.addBonus(user.id, 100);}

能够看到上述代码中register()开启了事务,然而在办法中又调用了另一个bonusServicebonusService没必要再创立新事务,事务的默认流传级别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<T> extends JdbcDaoSupport {    private String table;    private Class<T> entityClass;    private RowMapper<T> 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<T> getParameterizedType() {        Type type = this.getClass().getGenericSuperclass();        if (type instanceof ParameterizedType) {            ParameterizedType t = (ParameterizedType) type;            Type[] types = t.getActualTypeArguments();            Type firstType = types[0];            return (Class<T>) firstType;        }        return null;    }    public T getById(long id) {    }    public List<T> getAll(int pageIndex) {    }    public void deleteById(long id) {    }    public Class<T> getEntityClass() {        return entityClass;    }    public String getTable() {        return table;    }}

集成MyBatis

MyBatis是一款半自动ORM框架,只负责把ResultSet映射成JavaBean,SQL仍需本人编写

要应用它先要引入依赖:

<dependency>  <groupId>org.mybatis</groupId>  <artifactId>mybatis</artifactId>  <version>3.5.4</version></dependency><dependency>    <groupId>org.mybatis</groupId>    <artifactId>mybatis-spring</artifactId>    <version>2.0.4</version></dependency>

应用MyBatis的外围就是创立SqlSessionFactory

@BeanSqlSessionFactoryBean 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文件来做到,详情查看官网文档