乐趣区

关于java:Spring

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 注解:

@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<Validator> 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 在被注入后进行初始化操作以及容器敞开时进行销毁操作,需引入一个特定依赖

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

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

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

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

应用申明式事务:

@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() 开启了事务,然而在办法中又调用了另一个 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

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

退出移动版