SpringBoot深入理解


SpringBoot深入理解

項目打包SpringBoot啟動過程

當使用打包時,會下載org-springframework-boot-loader的jar,並且不會放在lib存放的第三方jar包文件中,該jar包中有個JarLauncher.class文件中設置了jar包運行時的入口和打包后文件的結構(定義了BOOT-INF,META-INF中放什么數據)

使用java -jar 啟動項目時,因為META-INF中文件記載了啟動的順序

Manifest-Version: 1.0		#版本
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx		
Start-Class: com.zyy.gradletest1.GradleTest1Application	     #項目的啟動器
Spring-Boot-Classes: BOOT-INF/classes/		#記錄編譯后文件存放地址
Spring-Boot-Lib: BOOT-INF/lib/						  #記錄第三方jar包存放地址
Spring-Boot-Version: 2.3.0.RELEASE		        #SpringBoot版本
Main-Class: org.springframework.boot.loader.JarLauncher			#標記了程序的入口,程序的入口定義類加載器去加載項目的啟動器

所以程序會直接進入JarLauncher.class中執行main方法

public class JarLauncher extends ExecutableArchiveLauncher {

	static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";

	static final String BOOT_INF_LIB = "BOOT-INF/lib/";

	public JarLauncher() {
	}

	protected JarLauncher(Archive archive) {
		super(archive);
	}

	@Override
	protected boolean isNestedArchive(Archive.Entry entry) {
		if (entry.isDirectory()) {
			return entry.getName().equals(BOOT_INF_CLASSES);
		}
		return entry.getName().startsWith(BOOT_INF_LIB);
	}

	public static void main(String[] args) throws Exception {
		new JarLauncher().launch(args);
	}

}

JarLancher繼承了ExecutableArchiveLauncher,ExecutableArchiveLauncher抽象類是歸檔啟動器的父類,它有兩個子類

​ Lancher:是整個歸檔的基類(抽象類)

​ |

ExecutableArchiveLauncher

​ | |

JarLancher WarLancher

顧名思義jarLancher就是打包成jar的歸檔啟動類,WarLancher是War打包方式的歸檔啟動類。

當執行JarLancher的main方法時,會調用Lancher基類的launch方法,該方法注釋為:啟動應用程序。 此方法是初始入口點應該由一個子類被稱為public static void main(String[] args)方法。

image-20200530094047755


使用SpringBoot自定義類加載器(LaunchedURLClassLoader)來加載打包好的Jar文件中BOOT-INF里面的類和lib

ClassLoader classLoader = createClassLoader(getClassPathArchives());

getClassPathArchives()方法:返回嵌套Archive S表示匹配指定過濾器條目。

意思就是:獲得需要自定義類加載器需要加載的類的路徑並返回(歸檔文件)

public abstract class ExecutableArchiveLauncher extends Launcher {
    @Override
    protected List<Archive> getClassPathArchives() throws Exception {
       List<Archive> archives = new ArrayList<>(
             /*this::isNestedArchive:確定指定JarEntry是應添加到類路徑嵌套項。 該方法被調用一次為每個條目。
                 說人話就是查看當前需要加載的路徑是否符合,如果符合返回true,這個方法調用的就是JarLancher類				  中的isNestedArchive方法*/
             this.archive.getNestedArchives(this::isNestedArchive));
       postProcessClassPathArchives(archives);
        //返回Archive類型的集合,Archive是可以由{@link Launcher}啟動的檔案類,里面記錄的需要加載類的路徑
       return archives;
    }
}

補充知識:

正常的jar包加載的方式就是直接找到classpath下的頂層路徑加載(例如:org.springframework)

傳統的打包方式是將第三方包和自己寫的代碼打包在一塊,這就有可能出現,包名沖突之類的。為了解決這種問題,Spring的打包方式就是把第三方包和自己的代碼分開。但是此時就出現了一個問題,不能正常找到頂層的加載文件,那么spring就出現了org-springframework-boot-loader包,spring默認規定該包在打包時將該包放到classpath下(頂層路徑),首先加載該類中對應的Lancher類,然后通過該類創建的自定義類加載器去加載項目中其他的類。


准備去加載項目的主運行程序

launch(args, getMainClass(), classLoader);

getMainClass():獲得ExecutableArchiveLauncher.Archive類中的清單,Archive在創建自定義類加載器時,被構造方法初始化,初始化的路徑就是classpath路徑下的META-INF文件中的信息然后讀取對應的主程序入口的路徑(也就是META-INF文件中的信息)key為Start-Class的值(就是項目主程序的啟動器)

public abstract class ExecutableArchiveLauncher extends Launcher {

	private final Archive archive;

	public ExecutableArchiveLauncher() {
		try {
			this.archive = createArchive();
		}
		catch (Exception ex) {
			throw new IllegalStateException(ex);
		}
	}
    @Override
    protected String getMainClass() throws Exception {
       Manifest manifest = this.archive.getManifest();
       String mainClass = null;
       if (manifest != null) {
          mainClass = manifest.getMainAttributes().getValue("Start-Class");
       }
       if (mainClass == null) {
          throw new IllegalStateException(
                "No 'Start-Class' manifest entry specified in " + this);
       }
       return mainClass;
    }
}

當所有參數准備完畢之后,進入launch方法中

public abstract class Launcher {
    /**
     * Launch the application given the archive file and a fully configured classloader.
     * @param args the incoming arguments
     * @param mainClass the main class to run
     * @param classLoader the classloader
     * @throws Exception if the launch fails
     */
    protected void launch(String[] args, String mainClass, ClassLoader classLoader)
          throws Exception {
        //將當前線程的上下文設置為自定義類加載器
       Thread.currentThread().setContextClassLoader(classLoader);
       createMainMethodRunner(mainClass, args, classLoader).run();
    }
}

創建一個運行Main的縣城,將運行產參數和類加載器傳入進去
createMainMethodRunner(mainClass, args, classLoader)

/**
 * Create the {@code MainMethodRunner} used to launch the application.
 * @param mainClass the main class					main方法運行的路徑
 * @param args the incoming arguments		  方法的參數
 * @param classLoader the classloader			 自定義類加載器
 * @return the main method runner				返回線程
 */
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args,
      ClassLoader classLoader) {
   return new MainMethodRunner(mainClass, args);
}

當線程創建好之后!!!重點來了!!!!運行run方法

createMainMethodRunner(mainClass, args, classLoader).run();

public void run() throws Exception {
    //獲得線程上下文取出類加載器,然后獲得要運行的main方法的路徑
   Class<?> mainClass = Thread.currentThread().getContextClassLoader()
         .loadClass(this.mainClassName);
    //通過反射定位到main方法的位置
   Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
    //調用對應的main方法(此時調用的就是SpringBoot項目的啟動器)
   mainMethod.invoke(null, new Object[] { this.args });
}

注意:mainMethod.invoke(null, new Object[] { this.args });

​ 為什么invoke第一個參數是空,因為main方法是static的,static不歸存與類中,它只是把類當做寄存的場所而已,原因是所有的方法必須存在與類中

jar包啟動時,使用-agentlib:jdwp遠程debug

什么是jdwep?

JDWP 是 Java Debug Wire Protocol 的縮寫,它定義了調試器(debugger)和被調試的 Java 虛擬機(target vm)之間的通信協議,可以項目在線運行時,遠程debug查看運行的流程,如果項目是部署在tomcat中運行,需要在tomcat中配置相關的啟動,才能使用遠程debug

啟動jar包時,使用命令

然后在idea中配置

注意:這種調試需要在配置之前在idea中有相同的代碼,然后打上斷點

注解

java.lang.annotation 提供了四種元注解,專門注解其他的注解(在自定義注解的時候,需要使用到元注解):
@Documented – 注解是否將包含在JavaDoc中
@Retention – 什么時候使用該注解
@Target – 注解用於什么地方
@Inherited – 是否允許子類繼承該注解

1.)@Retention – 定義該注解的生命周期
RetentionPolicy.SOURCE : 在編譯階段丟棄。這些注解在編譯結束之后就不再有任何意義,所以它們不會寫入字節碼。@Override, @SuppressWarnings都屬於這類注解。
RetentionPolicy.CLASS : 在類加載的時候丟棄。在字節碼文件的處理中有用。注解默認使用這種方式
RetentionPolicy.RUNTIME : 始終不會丟棄,運行期也保留該注解,因此可以使用反射機制讀取該注解的信息。我們自定義的注解通常使用這種方式。

2.)Target – 表示該注解用於什么地方。默認值為任何元素,表示該注解用於什么地方。可用的ElementType 參數包括
ElementType.CONSTRUCTOR: 用於描述構造器
ElementType.FIELD: 成員變量、對象、屬性(包括enum實例)
ElementType.LOCAL_VARIABLE: 用於描述局部變量
ElementType.METHOD: 用於描述方法
ElementType.PACKAGE: 用於描述包
ElementType.PARAMETER: 用於描述參數
ElementType.TYPE: 用於描述類、接口(包括注解類型) 或enum聲明

3.)@Documented – 一個簡單的Annotations 標記注解,表示是否將注解信息添加在java 文檔中。

4.)@Inherited – 定義該注釋和子類的關系
@Inherited 元注解是一個標記注解,@Inherited 闡述了某個被標注的類型是被繼承的。如果一個使用了@Inherited 修飾的annotation 類型被用於一個class,則這個annotation 將被用於該class 的子類。

@Configuration官方文檔

修飾一個類為配置類,將Bean注入到Spring容器中,在運行期間生成bean的定義

@Configuration
   public class AppConfig {
  
       @Bean
       public MyBean myBean() {
           // instantiate, configure and return bean ...
       }
   }

@Configuration類通常使用AnnotationConfigApplicationContext或者支持web的AnnotationConfigWebApplicationContext的引導,來使用@Bean

//在test類中可以使用,或者直接使用@Autowired自動注入
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
   ctx.register(AppConfig.class);
   ctx.refresh();
   MyBean myBean = ctx.getBean(MyBean.class);
   // use myBean ...

@PropertySource

@Configuration類可以使用@PropertySource注釋將屬性源貢獻給環境對象:

@Configuration
   @PropertySource("classpath:/com/acme/app.properties")
   public class AppConfig {
  
       @Inject Environment env;
  
       @Bean
       public MyBean myBean() {
           return new MyBean(env.getProperty("bean.name"));
       }
   }

@ComponentScan

@Configuration類不僅可以使用組件掃描引導,還可以自己使用@ComponentScan注釋配置組件掃描:

@Configuration
   @ComponentScan("com.acme.app.services")
   public class AppConfig {
       // various @Bean definitions ...
   }

@Import

@Configuration類可以使用@Import注釋組合,因為@Configuration對象在容器中被管理為Spring bean,所以導入的配置可能會被注入——例如,通過構造函數注入:

@Configuration

   public class DatabaseConfig {
  
       @Bean
       public DataSource dataSource() {
           // instantiate, configure and return DataSource
       }
   }
  
   @Configuration
   @Import(DatabaseConfig.class)
   public class AppConfig {
  
       private final DatabaseConfig dataConfig;
  
       public AppConfig(DatabaseConfig dataConfig) {
           this.dataConfig = dataConfig;
       }
  
       @Bean
       public MyBean myBean() {
           // reference the dataSource() bean method
           return new MyBean(dataConfig.dataSource());
       }
   }

@ContextConfiguration

組合多個@Configuration配置組件

@RunWith(SpringRunner.class)
   @ContextConfiguration(classes = {AppConfig.class, DatabaseConfig.class})
   public class MyTests {
  
       @Autowired MyBean myBean;
  
       @Autowired DataSource dataSource;
  
       @Test
       public void test() {
           // assertions against myBean ...
       }
   }

@Component

修飾一個類,表明一個注解的類是“部件”。@Configuration就是基於該注解。

@Autowired

自動注入,按照類型來匹配。

@Profile

分別開發環境和生產環境的配置

application.properties 配置文件

spring.profiles.active=development

測試環境

@Profile("development")
@Configuration
  public class EmbeddedDatabaseConfig {
        @Bean
          public DataSource dataSource() {
              // instantiate, configure and return embedded DataSource
          }
  }

生產環境

  @Profile("production")
  @Configuration
  public class ProductionDatabaseConfig {
    @Bean
      public DataSource dataSource() {
          // instantiate, configure and return production DataSource
      }
  }

第二種方法

@Configuration
  public class ProfileDatabaseConfig {
      //測試環境
      @Bean("dataSource")
      @Profile("development")
      public DataSource embeddedDatabase() { ... }
     //生產環境
      @Bean("dataSource")
      @Profile("production")
      public DataSource productionDatabase() { ... }
  }

@ImportResource

導入Spring的配置文件,讓配置文件里面的內容生效;Spring Boot里面沒有Spring的配置文件,我們自己編寫的配置文件

  •     例:@ImportResource(locations = {"classpath:beans.xml"})
    
  •     但是SpringBoott推薦給容器中添加組件的方式;推薦使用全注解的方式
    

@EnableAutoConfiguration

啟用Spring應用程序上下文的自動配置(開啟自動配置spring),當你不需要加載某個自定義類時可以在yml文件中:spring.autoconfigure.exclude: XXXX 來排除某個類。

@ComponentScan

配置使用組件掃描指令與@ Configuration類。 提供與Spring XML的支持並行context:component-scan元件。它包含了annotation-config這個屬性。

SpringBoot啟動過程

從main中執行SpringApplication.run(GradleTest1Application.class, args);

@SpringBootApplication
public class GradleTest1Application {

    public static void main(String[] args) {
        SpringApplication.run(GradleTest1Application.class, args);
    }

}

進入到SpirngApplication類中,然后在run方法中間接調用new SpringApplication的構造方法

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
   return run(new Class<?>[] { primarySource }, args);
}
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
   return new SpringApplication(primarySources).run(args);
}

SpringApplication(primarySources):源碼開始


public SpringApplication(Class<?>... primarySources) {
   this(null, primarySources);
}

從這里開始,有SpringApplication構造方法中嵌套的源碼

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
   this.resourceLoader = resourceLoader;		//資源加載器,因為上一次調用賦值為null,所以之這里時null
   Assert.notNull(primarySources, "PrimarySources must not be null");	  //斷言這里是否傳入的SpringBoot的啟動器是否為空
   this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));		//將SpringBoot啟動器放入一個Set<Class<?>>中
    /*WebApplicationType:Web應用程序可能類型的枚舉,里面有三個枚舉參數(查看Web是那種類型),deduceFromClasspath()方法中使用CLassUtils的class工具查看對應 的classpath路徑下是否有對應的配置,如果有返回對應的Web類型
    注意:源碼在本代碼塊后第一個代碼塊中
    */
   this.webApplicationType = WebApplicationType.deduceFromClasspath();
    /*加載默認配置類(ApplicationContextInitializer是初始化所有自動配置類的類),掃描所有自動配置jar包中“META-INF/spring.factories”的配置類jar包(
    	主要有org.springframework.boot.autoconfigure、org.springframework.boot.context、org.springframework.boot.beans)
       找到實現ApplicationContextInitializer接口的類,並實例化他們
       提示:源碼在本代碼塊后第二個代碼塊中
       */
   setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    //和上一行代碼操作一致,只不過是在三個jar包中找到實現ApplicationListener.class類並實例化它們
   setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    //找到主要的應用程序類
   this.mainApplicationClass = deduceMainApplicationClass();
    /**
    deduceMainApplicationClass();源碼說明:
    	private Class<?> deduceMainApplicationClass() {
		try {
			//直接new出一個運行異常,然后獲得堆信息
			StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
			//在堆信息中找到main方法
			for (StackTraceElement stackTraceElement : stackTrace) {
				if ("main".equals(stackTraceElement.getMethodName())) {
					//加載main方法,並返回class對象
					return Class.forName(stackTraceElement.getClassName());
				}
			}
		}
		catch (ClassNotFoundException ex) {
			// Swallow and continue
		}
		return null;
	}
    */
}

this.webApplicationType = WebApplicationType.deduceFromClasspath();源碼:

WebApplicationType:Web應用程序可能類型的枚舉,里面有三個枚舉參數(查看Web是那種類型),deduceFromClasspath()方法中使用CLassUtils的class工具查看對應 的classpath路徑下是否有對應的配置,如果有返回對應的Web類型

/**
 * An enumeration of possible types of web application.
 *
 * @author Andy Wilkinson
 * @author Brian Clozel
 * @since 2.0.0
 */
public enum WebApplicationType {

	/**
	 * The application should not run as a web application and should not start an
	 * embedded web server.
	 應用程序不應作為web應用程序運行,也不應該啟動嵌入式web服務器。
	 */
	NONE,

	/**
	 * The application should run as a servlet-based web application and should start an
	 * embedded servlet web server.
	 應用程序應作為基於servlet的web應用程序運行,並應啟動嵌入式servlet web服務器。
	 */
	SERVLET,

	/**
	 * The application should run as a reactive web application and should start an
	 * embedded reactive web server.
	 應用程序應該作為一個反應性web應用程序運行,並啟動一個嵌入式反應性web服務器。當前反應性web服務器在Spring5引進,它可以支持servlet容器,也可以支持servlet職位的容器
	 */
	REACTIVE;
    
    
    static WebApplicationType deduceFromClasspath() {
        //判斷類路徑是否存在,存在:true    不存在:false
       if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
             && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
          return WebApplicationType.REACTIVE;
       }
       for (String className : SERVLET_INDICATOR_CLASSES) {
          if (!ClassUtils.isPresent(className, null)) {
             return WebApplicationType.NONE;
          }
       }
       return WebApplicationType.SERVLET;
    }
}

setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); 源碼:

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
   return getSpringFactoriesInstances(type, new Class<?>[] {});
}

/**
重點!!!!!!!!!
這個類是返回在org.springframework.boot.autoconfigure、org.springframework.boot.context、org.springframework.boot.beans這三個包中繼承type類型的所有類,
這個類在SpringApplication中經常使用!!!!!!!
*/
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
    //獲得類加載器
   ClassLoader classLoader = getClassLoader();

    //獲得所有的自動配置類的路徑信息     
   Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
       /*     SpringFactoriesLoader.loadFactoryNames(type, classLoader)說明和源碼
       |															|										        	|		
       使用set集合為了確保惟一以防止重復,因為這里存放的是所有自動配置類(AutoConfiguration)
       loadFactoryNames(type, classLoader));    
      參數說明: (type = ApplicationContextInitializer.class(記錄被加載類的配置信息,並初始化)         classLoader = 系統加載器)
       
       方法調用了loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader)的同名方法,
       loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader)源碼:
       
       public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
            String factoryTypeName = factoryType.getName();
            //loadSpringFactories(classLoader):找到所有“META-INF/spring.factories”配置文件中的類
            //getOrDefault(factoryTypeName, Collections.emptyList());   過濾所有的類找出實現factoryTypeName的類並放入集合中		   				  (factoryTypeName=ApplicationContextInitializer.class
            return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
		}
		在return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());中獲得所有的自動配置類的路徑信息     
       */
    
    
    //在獲得所有需要初始化的自動配置類后,初始化他們
   List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
    //對類排序
   AnnotationAwareOrderComparator.sort(instances);
    //返回所有排序后的自動配置類集合
   return instances;
}

到這里結束


run(args):源碼開始


/**
 * Run the Spring application, creating and refreshing a new
 運行Spring應用程序,創建並刷新一個新應用程序
 * {@link ApplicationContext}.
 * @param args the application arguments (usually passed from a Java main method)
 * @return a running {@link ApplicationContext}
 */
public ConfigurableApplicationContext run(String... args) {
    //計時器(Spring創始人2001年寫的)
   StopWatch stopWatch = new StopWatch();
    //啟動計時器
   stopWatch.start();
    //ApplicationContext子類,封裝了配置和生命周期,防止ApplicationContext客戶端代碼看到它們
   ConfigurableApplicationContext context = null;
    //存放SpringApplication啟動運行時的異常
   Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    //以服務器模式運行,在系統可能缺少顯示設備、鍵盤或鼠標這些外設的情況下可以使用該模式,激活該模式:System.setProperty("java.awt.headless", "true");
   configureHeadlessProperty();
    /*SpringApplication運行時的監視器,每運行一次都會創建一個SpringApplicationRunListener的子類放入SpringApplicationRunListeners類中的List<SpringApplicationRunListener> 的集合中*/
   SpringApplicationRunListeners listeners = getRunListeners(args);
    /**     |                                              |											|			源碼:
    	private SpringApplicationRunListeners getRunListeners(String[] args) {
            Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
            return new SpringApplicationRunListeners(logger,
                    getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));	//找到繼承SpringApplicationRunListener類的所有類
		}
	    */
    
    
    //開始啟動所有繼承SpringApplicationRunListener的監視器
   listeners.starting();
   try {
       //加載默認參數,只是把args賦值給DefaultApplicationArguments類中的成員變量
      ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
       //配置環境,傳入所有監聽器和應用程序的參數
      ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
       //配置忽略一些Bean信息
      configureIgnoreBeanInfo(environment);
       //向控制台打印Banner(就是那個大的Spring字體,我們可以在classpath目錄下創建一個banner.txt文件,把想要打印的東西放入進去就可以在程序啟動時打印出來)
      Banner printedBanner = printBanner(environment);
       //創建應用上下文
      context = createApplicationContext();
       //獲取Spring工廠的實例
      exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
            new Class[] { ConfigurableApplicationContext.class }, context);
       //准備上下文,並輸出某些日志
      prepareContext(context, environment, listeners, applicationArguments, printedBanner);
       //刷新上下文,並輸出某個日志
      refreshContext(context);
       //刷新上下問后置處理
      afterRefresh(context, applicationArguments);
       //計時停止了
      stopWatch.stop();
      if (this.logStartupInfo) {
         new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
      }
      listeners.started(context);
      callRunners(context, applicationArguments);
   }
   catch (Throwable ex) {
      handleRunFailure(context, ex, exceptionReporters, listeners);
      throw new IllegalStateException(ex);
   }

   try {
      listeners.running(context);
   }
   catch (Throwable ex) {
      handleRunFailure(context, ex, exceptionReporters, null);
      throw new IllegalStateException(ex);
   }
   return context;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM