概述
spring的IOC容器初始化会做大量的工作,但是基本分为以下三个步骤:
1.定位:定位资源文件的位置,import、classpath、url(一般我们的资源文件都是在类路径classpath下)
2.加载:解析配置文件,把bean包装成BeanDefinition对象(BeanDefinition相当于是保存在内存中的配置文件,保存了所有跟类属性相关的信息。)
3.注册:将已经注册的BeanDefinition对象存入IOC容器中
IOC容器初始化完成之后,就是依赖注入了。
依赖注入就是把BeanDefinition里的信息读取出来,利用反射机制或者代理机制创建对象,一个bean对应一个BeanDefinition。新创建的对象不会放入我们印象中的IOC容器中,而是会存入到另外一个cache容器。所以,我们真实的读取bean是从cache容器中拿的。
下面先来实现spring的IOC容器初始化:
1》spring有很多的启动入口,这里自定义一个DispatchServlet作为启动入口,web.xml配置信息如下。
<web-app>
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>springMini</servlet-name>
<servlet-class>com.ft.spring.servlet.DispatchServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:application.properties</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springMini</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
另外,application.properties文件中的内容为:
scanPackage=com.ft.spring.demo
2》DispatchServlet类就是自己要模拟初始化IOC容器的类。
/**
* 因为我在web.xml里做了配置,所以当Tomcat的启动时会调用init(),
* 在init()里实现了自定义的定位、加载、注册及依赖注入方法。
*/
public class DispatchServlet extends HttpServlet{
private Properties contextConfig=new Properties();
//存储实例化的对象,当做IOC容器
private Map<String,Object> beanMap=new ConcurrentHashMap<String,Object>();
//存储扫描包后,所有的类名称
private List<String> classNames=new ArrayList<String>();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req,resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("================调用doPost================");
}
@Override
public void init(ServletConfig config) throws ServletException {
//初始化IOC容器
//定位 拿到配置we.xml里的classpath:application.properties
doLoadConfig(config.getInitParameter("contextConfigLocation"));
//加载
doScanner(contextConfig.getProperty("scanPackage"));
//注册
doRegistry();
//依赖注入
//在Spring中,是通过调用getBean方法才触发依赖注入的
doAutoWired();
//如果是springmvc会多一个HandlerMapping
//将@RequestMapping中配置的url和一个Method关联上
//以便于从浏览器获得用户输入的url后,能够找到具体执行的Method,然后反射去调用
}
//定位
private void doLoadConfig(String location) {
//在spring中,是通过Reader来查找定位的
//这里直接通过类加载器来加载资源文件application.properties,并得到一个输入流
InputStream is=this.getClass().getClassLoader().getResourceAsStream(location.replace("classpath:",""));
try {
//配置文件里的键值对加载到了Properties里面
contextConfig.load(is);
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(null != is){is.close();}
}catch (Exception e){
e.printStackTrace();
}
}
}
//加载
private void doScanner(String packageName) {//com.ft.spring.demo
//将类路径下com/ft/spring/demo的所有内容加载到url对象中
URL url=this.getClass().getClassLoader().getResource("/"+packageName.replaceAll("\\.","/"));
//得到demo的绝对路径,从而得到demo下的所有目录
File files=new File(url.getFile());
//类被加载后(.class),拿到这些类的全名,存入classNames里面,如:com.ft.spring.demo.action.DemoAction
for (File f:files.listFiles()){
if(f.isDirectory()){
doScanner(packageName+"."+f.getName());
}else{
classNames.add(packageName+"."+f.getName().replace(".class",""));
}
}
}
//注册
private void doRegistry() {
if(classNames.isEmpty()){
return;
}
try {
//将相关联的类都去实例化放入IOC容器,如类、类在另一个类中存在引用
for(String className:classNames){
Class<?> clazz=Class.forName(className);
//在Spring中用的多个子方法来处理的
if(clazz.isAnnotationPresent(Controller.class)){//如果该类加了Controller注解
//将类名首字母小写
String beanName=lowerFirstCase(clazz.getSimpleName());
// 在Spring中,这个阶段是不会直接put instance,这里put的是BeanDefinition
beanMap.put(beanName,clazz.newInstance());
}else if(clazz.isAnnotationPresent(Service.class)){//如果该类加了Service注解
Service service=clazz.getAnnotation(Service.class);
//这里需要分以下几种情况
//1.默认使用类名首字母小写注入
//2.如果自己定义了beanName,那么优先使用自己定义的beanName(@Service("xxx"))
//3.如果是一个接口,使用接口的类型去自动注入
//在Spring中同样会分别调用不同的方法 autowriedByName autowritedByType
String beanName=service.value();//拿@Service("xxx")里面的xxx
if("".equals(beanName.trim())){//如果没有自己定义beanName,则使用默认的
//当前的类名首字母小写作为beanName
beanName=lowerFirstCase(clazz.getSimpleName());
}
Object instance=clazz.newInstance();
beanMap.put(beanName,instance);
//如果是接口
Class<?>[] interfaces=clazz.getInterfaces();
for(Class<?> i:interfaces){
beanMap.put(i.getName(),instance);
}
}else{
continue;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
//依赖注入
private void doAutoWired() {
if(beanMap.isEmpty()){
return;
}
for(Map.Entry<String,Object> entry:beanMap.entrySet()){
//通过类的实例拿到类的反射,再取得类里面的所有字段信息
Field[] fields=entry.getValue().getClass().getDeclaredFields();
for(Field field:fields){
if(!field.isAnnotationPresent(Autowired.class)){
continue;
}
//如果该字段上加了Autowired
Autowired autowired=field.getAnnotation(Autowired.class);
String beanName=autowired.value().trim();//Autowired("xxx")
if("".equals(beanName)){
beanName = field.getType().getName();
}
field.setAccessible(true);
try {
field.set(entry.getValue(),beanMap.get(beanName));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
/**
* 字符串首字母小写
* @param str
* @return
*/
private String lowerFirstCase(String str){
char [] chars = str.toCharArray();
chars[0] += 32;
return String.valueOf(chars);
}
}
3》以上的依赖注入是采用注解方式的,而注解也是自定义的,各个自定义注解信息如下。
//作用在参字段上
@Target({ElementType.FIELD})
//注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
String value() default "";
}
//作用在类上
@Target({ElementType.TYPE})
//注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {
String value() default "";
}
//作用在类和方法上
@Target({ElementType.TYPE,ElementType.METHOD})
//注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
String value() default "";
}
//作用在参数上
@Target({ElementType.PARAMETER})
//注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
String value() default "";
}
//作用在类上
@Target({ElementType.TYPE})
//注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Service {
String value() default "";
}
总结:从这个案例中,可以清楚的了解到spring从启动到IOC容器初始化的过程,以及注解方式是怎么进行注入的。