意见箱
恒创运营部门将仔细参阅您的意见和建议,必要时将通过预留邮箱与您保持联络。感谢您的支持!
意见/建议
提交建议

Spring

来源:恒创科技 编辑:恒创科技编辑部
2024-01-28 00:45:59
IoC容器

Spring为我们提供了一个容器,用于管理所有的JavaBean组件,这是Spring框架最核心的概念

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

假设现在有一个MemberServiceBookService,现在它俩都需要操作数据库,用传统方式自然是在每个Service中都创建数据源实例,比如:


Spring

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="/news/upload/ueditor/image/202209/nejxlyi11yb"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="/news/upload/ueditor/image/202209/nejxlyi11yb
        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文件来做到,详情查看官方文档

上一篇: 110 个主流 Java 组件和框架整理,常用的都有,建议收藏!! 下一篇: 手机怎么远程登录云服务器?