Spring入门

SSM框架 = Spring+SpringMvc+MyBatis,我们从Spring开始学起!!!


1. 什么是框架

本文的Spring就是一种框架,框架在项目中的表现就是一系列的jar包,例如Thymeleaf就是一个框架。

每种框架都会解决某种特定的问题,可能是开发效率的问题,或运行效率的问题,或代码管理维护的问题等等。

项目中使用框架就相当于得到了一个“毛坯房”,使用了框架之后,开发人员只需要关心后续的“装修”即可。

绝大部分的框架都有特定的使用方式,在使用时,必须遵循框架的使用规则!

每个框架都可能是若干个开发人员甚至开发团队多年的工作积累的作品,对于初学者来说,不要过于钻牛角尖,尝试理解框架的底层实现原理!

简单的说:使用框架,可以让编程变得更加简单!在学习框架时,主要学习框架的正确使用方式!

2. 依赖关系

假设在项目中需要开发一个用户注册的功能!在项目中可能存在:

public class UserRegServlet {
    private UserDao userDao = new UserDao();
    
    public void doPost() {
        userDao.reg(); // 调用userDao对象实现存储用户数据
    }
}
public class UserDao {
    public void reg() {
        // 通过JDBC技术将用户数据存储到数据库中
    }
}

在以上代码中,UserRegServlet就是依赖UserDao的!

3. 耦合度

如果某个类过于依赖于另外一个类,通常称之为了“耦合度较高”,是不利于代码的管理和维护的,简单的说,如果UserRegServlet依赖于UserDao,在未来的某一天,UserDao已经不能满足项目的需求了(可能是因为代码有Bug,或者使用的技术比较落后等),如果需要把UserDao替换掉,替换难度大就会影响项目的管理和维护,为了解决这样的问题采取的解决方案就称之为“解耦”,使得依赖关系不那么明确,甚至就是不明确!

就以上UserRegServlet依赖UserDao的问题,如果要解耦,可以先创建一个接口:

public interface IUserDao {
    void reg();
}

然后,使得UserDao是实现了以上接口的:

public class UserDao implements IUserDao {
    public void reg() {
        // 具体的实现了reg()方法应该实现的功能
    }
}

经过以上调整以后,如果在UserRegServlet中需要使用到UserDao,以前的代码是这样的:

private UserDao userDao = new UserDao();

现在就可以改为:

private IUserDao userDao = new UserDao();

以上代码就相当于:

private List<String> strings = new ArrayList<>();

改成这样以后,在同一个项目中,无论多少个Servlet组件需要使用到UserDao,都可以使用以上“声明为接口,创建实现类的对象”的语法风格,如果以后UserDao需要被替换掉,也只需要替换“赋值”的代码,声明部分是不需要替换的!例如需要把UserDao替换为UserMybatisDao时,原来的代码是:

private IUserDao userDao = new UserDao();

新的代码就可以是:

public class UserMybatisDao implements IUserDao {
    public void reg() {
        // 使用更好的方式实现reg()应该实现的功能
    }
}

在后续的使用中,就可以是:

private IUserDao userDao = new UserMybatisDao();

也就是说,在UserDao换成了UserMybatisDao时,在各个Servlet中,都只需要调整等于号右侧的内容,而不再需要修改等于号左侧的部分!

当然,关于以上代码的右侧部分,还可以使用“工厂设计模式”作进一步的处理:

public class UserDaoFactory {
    // 返回接口类型的对象
    public static IUserDao newInstance() {
        return new UserDao(); // 也可以返回UserMybatisDao的对象
    }
}

当有了工厂后,此前的代码就可以进一步调整为:

private IUserDao userDao = UserDaoFactory.newInstance();

可以发现,以上代码中不再出现任何一个实现类的名字了,无论是哪个Servlet组件需要访问数据库,都声明为以上代码即可,以后,如果实现类需要被替换,也只需要替换工厂方法的返回值即可!

在实际项目开发时,项目中的组件的依赖更加复杂,为每个组件都创建对应的接口及工厂是非常麻烦的,而Spring框架就很好的解决了这个问题,可以简单的将Spring理解为一个“万能工厂”,当使用了Spring框架后,就不必自行开发工厂了!

4. Spring框架简介

Spring框架的主要作用:解决了创建对象和管理对象的问题。

5. 通过Spring创建对象

创建Maven Project,在创建过程中,勾选Create a simple projectGroup Id填为反写的公司域名如cn.teduArtifact Id填为spring01,其它项保持默认即可。
在这里插入图片描述
在这里插入图片描述

使用Spring框架时,必须在项目的pom.xml中添加spring-context的依赖:

<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.5.RELEASE</version>
</dependency>

首先,在项目中,创建cn.tedu.spring包,并在这个包下创建BeanFactory类:

package cn.tedu.spring;

public class BeanFactory {

}

当前,代码放在哪个包中并不重要,应该养成习惯,每个类都应该放在某个包中,不要让任何类不放在任何包中!

以上类的名称也不重要,是自定义的!

如果希望由Spring来创建并管理某个类的对象,必须在以上类中添加方法,关于这个方法:

  • 应该使用public权限;
  • 返回值类型就是需要Spring创建并管理的类的对象的类型;
  • 方法名称可以自定义;
  • 参数列表暂时为空;
  • 在方法体中,自行编写创建返回值对象的代码。

假设需要Spring来创建Date类型的对象,则在类中添加方法:

public Date aaa() {
    // 规范,规则
}

Spring框架要求:创建对象的方法必须添加@Bean注解,并且,这样的方法必须在配置类中!任何一个类添加了@Configuration注解都可以作为配置类!

package cn.tedu.spring;

@Configuration
public class BeanFactory {
	
	@Bean
	public Date aaa() {
		return new Date();
	}

}

完成后,应该使用一个可以运行的类,或通过单元测试来检验“是否可以通过Spring容器获取对象”。本次先创建一个可以运行的类:

package cn.tedu.spring;

public class SpringTests {

	public static void main(String[] args) {
		// 1. 加载配置类,得到Spring容器
		AnnotationConfigApplicationContext ac
			= new AnnotationConfigApplicationContext(BeanFactory.class);
		
		// 2. 从Spring容器中获取所需要的对象
		Date date = (Date) ac.getBean("aaa"); // getBean()方法的参数就是创建对象的方法的名称
		
		// 3. 测试获取到的对象
		System.out.println(date);
		
		// 4. 关闭
		ac.close();
	}

}

6. 关于@Bean注解

当方法的声明之前添加了@Bean注解,就表示这个方法是需要由Spring框架所调用,并且,由Spring框架管理该方法返回的对象的!默认情况下,该方法的名称就是后续获取对象时,调用getBean()方法的参数!

由于添加了@Bean注解的方法是被Spring框架调用的,不需要自行编写代码来调用这个方法,所以,Spring的建议是“使用合理的属性名称作为方法名,并不需要使用动词或动词为前缀的方法名”,简单的说,如果方法是为了获取Date类型的对象的,该方法的名称应该是date,而不是getDate(),则后续调用getBean()时,参数就是date这个名称!

当然,如果不遵循Spring的建议,还可以在@Bean注解中配置注解参数来指定Bean的名称,例如:

@Bean("date")
public Date getDate() {
    return new Date();
}

则后续就根据注解参数来获取对象:

Date date = (Date) ac.getBean("date");

其关系如下图:
在这里插入图片描述

其实,在开发项目时,真的不必关心这些问题,也就是说,例如是一个获取Date对象的方法,其名称到底是date还是getDate都是正确的!毕竟这个方法最终就是由Spring框架来调用,开发人员不会自行调用该方法!

7. Spring管理对象的作用域

由Spring管理的对象,默认情况下,是单例的!所以,其作用域就非常久!

在Spring管理对象的情况下,讨论对象的作用域,其本质就是讨论其是否单例!

在创建对象的方法之前,添加@Scope注解,并配置注解参数为prototype,就可以使得该对象不是单例的:

@Scope("prototype")
@Bean
public User user() {
    return new User();
}

由Spring管理的对象,如果是单例模式的,默认情况下,是饿汉式的!在创建对象的方法之前,添加@Lazy注解,就可以调整为懒汉式的:

@Bean
@Lazy
public User user() {
    return new User();
}

一般,在开发项目时,极少调整对象的作用域!

8. 小结:

  • Spring的主要作用:创建对象,管理对象;
  • 如果某个方法是用于给Spring框架创建对象的,这个方法就必须添加@Bean注解;
  • 所有添加了@Bean注解的方法,其所在的类应该添加@Configuration注解,凡添加了@Configuration注解的类称之为配置类
  • 默认情况下,由Spring管理的对象是单例的,使用@Scope注解可以将Spring管理的对象调整为“非单例”的;
  • 默认情况下,由Spring管理的单例的对象是是“饿汉式”的,使用@Lazy可以将它们改为“懒汉式”的。

附1:设计模式之单例模式

单例模式的特点:在同一时期,某个类的对象一定最多只有1个!也许会尝试多次的获取对象,但是,获取到的一定是同一个对象!

假设项目中有King类:

public class King {
}

很显然,目前它并不是单例的,因为,可以:

King k1 = new King();
King k2 = new King();
King k3 = new King();

以上代码就创建了3个King类型的对象!如果要实现单例,首先,就必须限制构造方法的访问,例如:

public class King {
    //构造函数是用于初始化对象用的,将构造函数私有化了之后,new对象时,对象不能访问构造函数,会导致对象初始化失败。
    private King() {
    }
}

每个类中都可以有若干个构造方法,如果某个类没有显式的声明任何构造方法,编译器就会自动添加1个公有的、无参数的构造方法!如果类中已经声明任何构造方法,则编译器不会自动添加构造方法!

由于将构造方法声明为私有的,则原有的King k1 = new King();这类代码就不能用于创建对象了!

限制构造方法的访问,其目的是“不允许随意创建对象”,并不是“不允许创建对象”,在King类的内部,还是可以创建对象的,可以添加方法,返回内部创建的对象:

public class King {
    private King king = new King();
    
    private King() {
    }
    
    public King getInstance() {
        return king;
    }
}

所以,当需要King类型的对象时,可以通过getInstance()方法来获取!

但是,以上代码是不可行的!因为,如果要调用getInstance()方法,必须先获取King的对象,而获取King对象的唯一方式就是调用getInstance()方法!为了解决这个问题,必须在getInstance()方法的声明之前添加static修饰符,最终,就可以通过类名.方法名()的语法格式来调用方法了!同时,由于“被static修饰的成员,不可以访问其它未被static修饰的成员”,所以,全局属性king也必须被static修饰:

public class King {
    private static King king = new King();
    
    private King() {
    }
    
    public static King getInstance() {
        return king;
    }
}

至此,基本的单例模式的代码就设计完成了!

以上代码是“饿汉式”的单例模式,另外,还有“懒汉式”的单例模式!

基本的懒汉式单例模式的代码是:

public class King {
    private static King king = null;
    
    private King() {
    }
    
    public static King getInstance() {
        if (king == null) {
            king = new King();
        }
        return king;
    }
}

注意:以上代码是多线程不安全的!

在开发领域中,只要数据的产生、变化不是开发人员预期的,就称之为“不安全”,也就是“数据安全问题”。

为了保障线程安全,应该为以上创建对象的代码片断“加锁”,例如:

public class King {
    private static King king = null;
    
    private King() {
    }
    
    public static King getInstance() {
        synchronized ("hello") {
            if (king == null) {
                king = new King();
            }
        }
        return king;
    }
}

当然,无论是哪个线程在什么时候执行以上代码,都必须先“锁住”代码片断后才能开始执行,是没有必要的,“锁”的性能消耗是浪费的,所以,可以进一步调整为:

public class King {
    private static King king = null;
    
    private King() {
    }
    
    public static King getInstance() {
        if (king == null) { // 判断有没有必要锁定接下来的代码
            synchronized ("java") {
                if (king == null) { // 判断有没有必要创建对象
                    king = new King();
                }
            }
        }
        return king;
    }
}

至此,懒汉式的单例模式就完成了!


附2:原型与单例模式

原型模式(多例),单例是全局就一个对象,将对象的属性修改后,再次获取的对象值是修改过的,而原型的值是都相同的在这里插入图片描述

原型模式

在这里插入图片描述

9. Spring管理对象的生命周期

如果需要管理Bean的生命周期,可以在对应的类中自定义生命周期的初始化方法和销毁方法,关于这2个方法的声明:

  • 应该使用public权限;
  • 使用void表示返回值类型;
  • 方法名称可以自定义;
  • 参数列表为空。
    例如:
package cn.tedu.spring;

public class User {
	
	public User() {
		System.out.println("User.User()");
	}
	
	public void init() {
		System.out.println("User.init()");
	}
	
	public void destroy() {
		System.out.println("User.destroy()");
	}

}

在配置Spring管理对象的@Bean注解中,配置注解参数,以指定以上2个方法分别是初始化方法和销毁方法:

package cn.tedu.spring;

@Configuration
public class BeanFactory {

	@Bean(initMethod = "init", destroyMethod = "destroy")
	public User user() {
		return new User();
	}
	
}

最终,可以看到:

  • 初始化方法会在构造方法之后执行,且只执行1次;
  • 销毁方法会在Spring容器被销毁之前执行,且只执行1次。

10. 使用组件扫描使得Spring管理类的对象

首先,自定义某个类(类名、包名均没有要求),在类的声明之前添加@ComponentScan注解,该注解用于配置组件扫描,注解的参数是String类型的,表示“被扫描的根包”:

package cn.tedu.spring;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan("cn.tedu.spring")
public class SpringConfig {

}

在组件扫描的包下创建类,该类的声明之前需要添加@Component注解,以表示这个类是一个“组件类”,后续,当Spring扫描时,会自动创建所有组件类的对象:

package cn.tedu.spring;

import org.springframework.stereotype.Component;

@Component
public class User {

}

当完成以后配置后,后续,程序执行时,只要加载了SpringConfig类,由于类之前配置了组件扫描,Spring框架就会扫描对应的包下所有的类,并逐一检查是否为“组件类”,如果是,则创建对象,如果不是,则不创建!

使用@ComponentScan时,配置的是需要扫描的“根包”,假设需要扫描的是cn.tedu.spring,在配置时,配置为cn.tedu甚至配置为cn都是可用的,但是,强烈不推荐使用过于简单的设置,避免出现扫描范围过多而导致的浪费资源!

另外,在@ComponentScan注解的源代码中:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {

	@AliasFor("basePackages")
	String[] value() default {};

	@AliasFor("value")
	String[] basePackages() default {};
    
}

可以看出,配置的值可以是String[],也就是可以指定多个包名。

在使用这种做法时,必须保证被Spring管理的对象所归属的类存在无参数构造方法!

在使用这种做法时,Spring创建对象后,默认会使用以下原则作为Bean的名称:

  • 如果类名的第1个字母是大写的,第2个字母是小写的(不关心其它字母的大小写),则会把类名的第1个字母改为小写,其它不变,作为Bean的名称,例如类名是User时,Bean的名称就是user,类名是UserDao时,Bean的名称就是userDao
  • 如果不满足以上条件,则类名就是Bean的名称。

如果希望使用自定义的名称作为Bean的名称,可以在@Component注解中配置参数,例如:

package cn.tedu.spring;

import org.springframework.stereotype.Component;

@Component("uuu")
public class User {

}

则后续调用getBean()方法时,就必须使用"uuu"作为参数来获取对象!

11. 自动装配

Spring可以将容器中存在的任何对象自动赋值到某个属性或参数中去!这样的机制就称之为“自动装配”!简单的说,当某个属性或参数需要值时,只要这个值是在Spring容器中,Spring就可以为它自动赋值!

在需要被自动装配的属性之前添加@Autowired注解,即表示“希望Spring为该属性自动的装配值”,例如:

@Controller
public class UserLoginServlet {

	@Autowired
	private UserDao userJdbcDao;

	public void doPost() {
		System.out.println("UserLoginServlet.doPost()");
		userJdbcDao.login();
	}

}

当然,自动装配的前提是Spring容器中有合适的值!以上需要被装配的是UserDao类型的属性,那就要求某个类是实现了UserDao接口的,并且这个类是被Spring管理的(组件扫描+注解),例如:

package cn.tedu.spring;

import org.springframework.stereotype.Repository;

@Repository
public class UserJdbcDao implements UserDao {
	
	public void reg() {
		System.out.println("UserJdbcDao.reg()");
	}
	
	public void login() {
		System.out.println("UserJdbcDao.login()");
	}

}

Spring框架在实现自动装配时,有2种装配模式:

  • byName模式:根据名称实现自动装配,在这种模式下,要求被装配的属性的名称,与Bean的名称是完全一致的!
  • byType模式:根据类型实现自动装配,在这种模式下,要求被装配的属性,在Spring容器中存在类型匹配的对象,如果被装配的属性是声明为父级类型,则Spring容器存在子级类型的对象即可装配成功,如果被装配的属性是声明为接口类型,则Spring容器存在实现类的对象即可装配成功!注意:使用这种模式时,如果在Spring容器中匹配类型的对象超过1个,就会装配失败!

当使用@Autowired注解时,其装配机制是:首先,会以byType模式在Spring容器中查找匹配类型的对象的数量,如果为0个,会报告错误,例如:

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'cn.tedu.spring.UserDao' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

如果有1个,则直接装配,无视名称是否匹配;

如果超过1个,会尝试通过byName模式来装配,如果名称匹配成功,则实现装配,如果名称均不匹配,则报告错误,例如:

Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'cn.tedu.spring.UserDao' available: expected single matching bean but found 2: userJdbcDao,userMybatisDao

当然,不可以存在2个对象使用相同的名称,否则,会因为名称冲突而出现错误:

Caused by: org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'a' for bean class [cn.tedu.spring.UserMybatisDao] conflicts with existing, non-compatible bean definition of same name and class [cn.tedu.spring.UserJdbcDao]

另外,还可以使用@Resource注解来修饰被装配的属性,例如:

@Controller
public class UserLoginServlet {

	@Resource // 这里既可以使用@Autowired,也可以使用@Resource
	private UserDao userMybatisDao;

	public void doPost() {
		System.out.println("UserLoginServlet.doPost()");
		userMybatisDao.login();
	}

}

使用@Resource时,必须将环境改为JDK 1.8,或添加Tomcat环境。

使用@Resource时,其执行效果与@Autowired是完全一致的!但是,其装配机制并不相同!@Resource的装配机制是:先尝试byName模式来装配,如果存在名称匹配的对象,则直接装配,如果不存在,则尝试byType模式来装配。

@Resource注解的功能和@Autowired

将我们之前编写的代码中的@Autowired注解替换为@Resource一般情况下都是可以正常运行的

也就是说由@Resource标注的属性也会进行自动装配

他们的区别是:

1.提供者不同:@Autowired是Spring提供的@Resource是java提供的

2.注入规则不同:

原则上@Autowired注入规则为"byType"(通过类型)

​ @Resource注入规则为"byName"(通过名称)这里的名称就是对象的id

@Autowired是先检查类型,如果有类型匹配直接匹配,只通过类型不能匹配了,再通过id

@Resource是先匹配id,如果id匹配直接成功,如果没有id匹配再匹配类型

12. 使用Spring获得properties文件信息

首先将我们的jdbc.properties文件复制到我们的resources文件夹

步骤2:

pom.xml文件中添加mysql和druid连接池的依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.21</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.19</version>
</dependency>

步骤3:

使用Spring提供的功能获得properties文件中的信息

注意如果jdbc.properties是复制到idea中的,可能需要ReBuild项目

//指定配置文件的位置
//classpath实际上就是指resources文件夹
@PropertySource("classpath:jdbc.properties")
public class Config {

    //这个自动装配是Spring获取Spring中自带的Environment类型对象
    //这个对象会对@PropertySource声明的配置文件进行解析
    @Autowired
    Environment env;
    @Bean
    public DataSource dataEnv(){
        System.out.println(env);
        DruidDataSource ds=new DruidDataSource();
        //使用env对象为ds设置参数
        ds.setUrl(env.getProperty("db.url"));
        ds.setDriverClassName(env.getProperty("db.driver"));
        ds.setUsername(env.getProperty("db.username"));
        ds.setPassword(env.getProperty("db.password"));
        ds.setMaxActive(
                env.getProperty("db.maxActive",Integer.class));
        ds.setInitialSize(
                env.getProperty("db.initialSize",Integer.class));
        return ds;
    }

}

然后就可以测试了

测试代码如下

 @Test
    public void test() throws SQLException {
        DataSource ds=ctx.getBean("dataEnv",DataSource.class);
        try(Connection conn=ds.getConnection()){
            String sql="select username from vrduser where id=4";
            PreparedStatement ps=conn.prepareStatement(sql);
            ResultSet rs=ps.executeQuery();
            while(rs.next()){
                System.out.println(rs.getString(1));
            }
        }
    }

实际上还有另外一种方式获得properties文件中的信息

代码如下

@PropertySource("classpath:jdbc.properties")
public class ConfigValue {

    //这个方法无需自动装配Environment对象

    //@Value实际上会自动从@PropertySource指定的配置文件中获得信息
    //@Value("${}")  ${}中的内容是properties文件中的name(键)
    // 这个键对应的值会自动赋值到@Value注解之后的参数中!
    @Bean
    public DataSource dataValue(
            @Value("${db.driver}") String driver,
            @Value("${db.url}") String url,
            @Value("${db.username}") String username,
            @Value("${db.password}") String password,
            @Value("${db.maxActive}") int maxActive,
            @Value("${db.initialSize}") int initialSize
    ){
        DruidDataSource ds=new DruidDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(username);
        ds.setPassword(password);
        ds.setMaxActive(maxActive);
        ds.setInitialSize(initialSize);
        return ds;
    }

}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

boy快快长大

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值