1. 基本概念
如今,java的技术体系已经涉及到方方面面,囊括web前端/后端/移动/大数据等方面。
- Java SE是标准版,包含标准的JVM和标准库。
- Java EE是企业版,它在Java SE的基础上加上了大量的API和库,以便方便开发Web应用、数据库、消息服务等,包括Spring框架、数据库开发、分布式架构等。
- JRE:Java Runtime Environment,运行Java字节码的虚拟机
- JDK:Java Development Kit,JDK除了包含JRE,还提供了编译器、调试器等开发工具
- JAR 文件的全称 Java Archive File(Java 档案文件),通常 JAR 文件是一种压缩格式,和 ZIP 格式兼容。我们类文件打包成一个 JAR 包,把这个 JAR 包提供给别人使用,有点类似C语言的静态库。java生态的工具类都是以 jar 包的形式出现的,例如 Spring,SpringMVC、MyBatis、数据库驱动等等。
- JavaSE程序可以打包成Jar包,而JavaWeb程序可以打包成war包。然后把war发布到Tomcat的webapps目录下,Tomcat会在启动时自动解压war包。
- Maven 是一个项目管理工具,依赖管理,项目编译、测试、打包、部署、上传等。类似go module, rust cargo,是现代语言必备的工具。
2. java语法
2.1 基本数据类型
- 整数类型:byte,short,int,long
- 浮点数类型:float,double
- 字符类型:char
- 布尔类型:boolean
public class Test {
static boolean bool;
static byte by;
static char ch;
static double d;
static float f;
static int i;
static long l;
static short sh;
static String str;
public static void main(String[] args) {
System.out.println("Bool :" + bool);
System.out.println("Byte :" + by);
System.out.println("Character:" + ch);
System.out.println("Double :" + d);
System.out.println("Float :" + f);
System.out.println("Integer :" + i);
System.out.println("Long :" + l);
System.out.println("Short :" + sh);
System.out.println("String :" + str);
}
}
2.2 引用类型
String s = "hello"; // 字符串
final double PI = 3.14; // PI是一个常量
var sb = new StringBuilder(); // 如果想省略变量类型,可以使用var关键字
int[] ns = new int[5]; // 数组
String[] names = {"ABC", "XYZ", "zoo"}; // 字符数组
int[][] ns = {
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 }
}; // 二维数组
数组的遍历
// 标准for循环
public class Main {
public static void main(String[] args) {
int[] ns = { 1, 4, 9, 16, 25 };
for (int i=0; i<ns.length; i++) {
int n = ns[i];
System.out.println(n);
}
}
}
// for each
public class Main {
public static void main(String[] args) {
int[] ns = { 1, 4, 9, 16, 25 };
for (int n : ns) {
System.out.println(n);
}
}
}
2.3 修饰符
修饰符用来定义类、方法或者变量,通常放在语句的最前端。
访问控制修饰符
Java中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。
- default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
- private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
- public : 对所有类可见。使用对象:类、接口、变量、方法
- protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
非访问修饰符
为了实现一些其他的功能,Java 也提供了许多非访问修饰符。
- static 修饰符,用来修饰类方法和类变量。
- final 修饰符,用来修饰类、方法和变量,final 修饰的类不能够被继承,修饰的方法不能被继承类重新定义,修饰的变量为常量,是不可修改的。
- abstract 修饰符,用来创建抽象类和抽象方法。
- synchronized 和 volatile 修饰符,主要用于线程的编程。
2.4 流程控制
和c/c++一脉相承,不多说。
2.5 异常处理
try {
String s = processFile(“C:\\test.txt”);
// ok:
} catch (FileNotFoundException e) {
// file not found:
} catch (SecurityException e) {
// no read permission:
} catch (IOException e) {
// io error:
} catch (Exception e) {
// other error:
}
3. 面向对象
3.1 构造方法
public class Main {
public static void main(String[] args) {
Person p = new Person("Xiao Ming", 15);
System.out.println(p.getName());
System.out.println(p.getAge());
}
}
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
}
3.2 继承
class 父类 {
}
// 继承有个特点,就是子类无法访问父类的private字段或者private方法
// protected关键字可以把字段和方法的访问权限控制在继承树内部,一个protected字段和方法可以被其子类,以及子类的子类所访问
class 子类 extends 父类 {
}
3.3 接口(英文:Interface)
// 在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法。
/* 文件名 : Animal.java */
interface Animal {
public void eat();
public void travel();
}
/* 文件名 : MammalInt.java */
public class MammalInt implements Animal{
public void eat(){
System.out.println("Mammal eats");
}
public void travel(){
System.out.println("Mammal travels");
}
public int noOfLegs(){
return 0;
}
public static void main(String args[]){
MammalInt m = new MammalInt();
m.eat();
m.travel();
}
}
3.4 包(package)
// package把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用
package net.java.util;
public class Something{
...
}
// 为了能够使用某一个包的成员,我们需要在 Java 程序中明确导入该包。使用 "import" 语句可完成此功能。
import net.java.util;
3.5 反射
// 反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法
public class Apple {
private int price;
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public static void main(String[] args) throws Exception{
//正常的调用
Apple apple = new Apple();
apple.setPrice(5);
System.out.println("Apple Price:" + apple.getPrice());
//使用反射调用
Class clz = Class.forName("com.chenshuyi.api.Apple");
Method setPriceMethod = clz.getMethod("setPrice", int.class);
Constructor appleConstructor = clz.getConstructor();
Object appleObj = appleConstructor.newInstance();
setPriceMethod.invoke(appleObj, 14);
Method getPriceMethod = clz.getMethod("getPrice");
System.out.println("Apple Price:" + getPriceMethod.invoke(appleObj));
}
}
3.6 泛型
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
java的很多集合(比如List,Map等)都是基于泛型搭建的。
public class GenericMethodTest
{
// 泛型方法 printArray
public static < E > void printArray( E[] inputArray )
{
// 输出数组元素
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
public static void main( String args[] )
{
// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3, 4, 5 };
Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };
System.out.println( "整型数组元素为:" );
printArray( intArray ); // 传递一个整型数组
System.out.println( "\n双精度型数组元素为:" );
printArray( doubleArray ); // 传递一个双精度型数组
System.out.println( "\n字符型数组元素为:" );
printArray( charArray ); // 传递一个字符型数组
}
}
// 和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。
public class Box<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
Box<String> stringBox = new Box<String>();
integerBox.add(new Integer(10));
stringBox.add(new String("菜鸟教程"));
System.out.printf("整型值为 :%d\n\n", integerBox.get());
System.out.printf("字符串为 :%s\n", stringBox.get());
}
}
4. java web开发历史发展
4.1 servlet
编写HTTP服务器其实是非常简单的,只需要先编写基于多线程的TCP服务,然后在一个TCP连接中读取HTTP请求,发送HTTP响应即可。
但是,要编写一个完善的HTTP服务器,以HTTP/1.1为例,需要考虑的包括:
- 识别正确和错误的HTTP请求;
- 识别正确和错误的HTTP头;
- 复用TCP连接;
- 复用线程;
- IO异常处理;
- …
这些基础工作需要耗费大量的时间,并且经过长期测试才能稳定运行。如果我们只需要输出一个简单的HTML页面,就不得不编写上千行底层代码,那就根本无法做到高效而可靠地开发。
因此,在JavaEE平台上,处理TCP连接,解析HTTP协议这些底层工作统统扔给现成的Web服务器去做,我们只需要把自己的应用程序跑在Web服务器上。为了实现这一目的,JavaEE提供了Servlet API,我们使用Servlet API编写自己的Servlet来处理HTTP请求,Web服务器实现Servlet API接口,实现底层功能。
下面是一个最简单的Servlet:
// WebServlet注解表示这是一个Servlet,并映射到地址/:
@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 设置响应类型:
resp.setContentType("text/html");
// 获取输出流:
PrintWriter pw = resp.getWriter();
// 写入响应:
pw.write("<h1>Hello, world!</h1>");
// 最后不要忘记flush强制输出:
pw.flush();
}
}
一个Servlet总是继承自HttpServlet,然后覆写doGet()或doPost()方法。注意到doGet()方法传入了HttpServletRequest和HttpServletResponse两个对象,分别代表HTTP请求和响应。我们使用Servlet API时,并不直接与底层TCP交互,也不需要解析HTTP协议,因为HttpServletRequest和HttpServletResponse就已经封装好了请求和响应。以发送响应为例,我们只需要设置正确的响应类型,然后获取PrintWriter,写入响应即可。
程序写好,运行Maven命令mvn clean package,在target目录下得到一个hello.war文件,这个文件就是我们编译打包后的Web应用程序。
普通的Java程序是通过启动JVM,然后执行main()方法开始运行。但是Web应用程序有所不同,我们无法直接运行war文件,必须先启动Web服务器,再由Web服务器加载我们编写的HelloServlet,这样就可以让HelloServlet处理浏览器发送的请求。常用的web服务器有:Tomcat,Jetty等。
一个完整的Web应用程序的开发流程如下:
- 编写Servlet;
- 打包为war文件;
- 复制到Tomcat的webapps目录下;
- 启动Tomcat。
实际上,类似Tomcat这样的服务器也是Java编写的,启动Tomcat服务器实际上是启动Java虚拟机,执行Tomcat的main()方法,然后由Tomcat负责加载我们的.war文件,并创建一个HelloServlet实例,最后以多线程的模式来处理HTTP请求。如果Tomcat服务器收到的请求路径是/(假定部署文件为ROOT.war),就转发到HelloServlet并传入HttpServletRequest和HttpServletResponse两个对象。
因为我们编写的Servlet并不是直接运行,而是由Web服务器加载后创建实例运行,所以,类似Tomcat这样的Web服务器也称为Servlet容器。
在Servlet容器中运行的Servlet具有如下特点:
- 无法在代码中直接通过new创建Servlet实例,必须由Servlet容器自动创建Servlet实例;
- Servlet容器只会给每个Servlet类创建唯一实例;
- Servlet容器会使用多线程执行doGet()或doPost()方法。
一个Servlet类在服务器中只有一个实例,但对于每个HTTP请求,Web服务器会使用多线程执行请求。因此,一个Servlet的doGet()、doPost()等处理请求的方法是多线程并发执行的。如果Servlet中定义了字段,要注意多线程并发访问的问题:
public class HelloServlet extends HttpServlet {
private Map<String, String> map = new ConcurrentHashMap<>();
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 注意读写map字段是多线程并发的:
this.map.put(key, value);
}
}
4.2 JSP开发
Servlet就是一个能处理HTTP请求,发送HTTP响应的小程序,而发送响应无非就是获取PrintWriter,然后输出HTML。
只不过,用PrintWriter输出HTML比较痛苦,因为不但要正确编写HTML,还需要插入各种变量。如果想在Servlet中输出一个类似新浪首页的HTML,写对HTML基本上不太可能。
我们可以使用JSP:
- JSP是一种在HTML中嵌入动态输出的文件,它和Servlet正好相反,Servlet是在Java代码中嵌入输出HTML;
- JSP可以引入并使用JSP Tag,但由于其语法复杂,不推荐使用;
- JSP本身目前已经很少使用,我们只需要了解其基本用法即可。
4.3 MVC开发
- Servlet适合编写Java代码,实现各种复杂的业务逻辑,但不适合输出复杂的HTML;
- JSP适合编写HTML,并在其中插入动态内容,但不适合编写复杂的Java代码。
能否将两者结合起来,发挥各自的优点,避免各自的缺点?
其中一个答案是,可以使用MVC模式:
- 使用MVC模式的好处是,Controller专注于业务处理,它的处理结果就是Model。
- Model可以是一个JavaBean,也可以是一个包含多个对象的Map
- Controller只负责把Model传递给View,View只负责把Model给“渲染”出来
- 这样,三者职责明确,且开发更简单,因为开发Controller时无需关注页面,开发View时无需关心如何创建Model。
比较有名的mvc开发框架就是Spring MVC, 但是mvc模式在现代的web开发中也很少使用了,我们了解一下即可。
4.4 Rest接口
现在java web开发都是前后端分离,前端只需要关注页面呈现,后端只需要实现rest接口。
使用Spring框架可以很方便地开发rest接口,它提供了一个@RestController注解,每个方法自动变成API接口方法
@RestController
@RequestMapping("/api")
public class ApiController {
@Autowired
UserService userService;
@GetMapping("/users")
public List<User> users() {
return userService.getUsers();
}
@GetMapping("/users/{id}")
public User user(@PathVariable("id") long id) {
return userService.getUserById(id);
}
@PostMapping("/signin")
public Map<String, Object> signin(@RequestBody SignInRequest signinRequest) {
try {
User user = userService.signin(signinRequest.email, signinRequest.password);
return Map.of("user", user);
} catch (Exception e) {
return Map.of("error", "SIGNIN_FAILED", "message", e.getMessage());
}
}
public static class SignInRequest {
public String email;
public String password;
}
}
5. Spring 框架系列
Spring框架系列,是java服务端开发,当之无愧的霸主了,使用Spring框架开发,可以让开发者专注于业务开发,极大提高了开发效率。
而且,随着Spring越来越受欢迎,在Spring Framework基础上,又诞生了Spring Boot、Spring Cloud、Spring Data、Spring Security等一系列基于Spring Framework的项目。
5.1 SpingFramework
Spring框架最核心的概念就是容器。
什么是容器?容器是一种为某种特定组件的运行提供必要支持的一个软件环境。例如,Tomcat就是一个Servlet容器,它可以为Servlet的运行提供运行环境。
通常来说,使用容器运行组件,除了提供一个组件运行环境之外,容器还提供了许多底层服务。例如,Servlet容器底层实现了TCP连接,解析HTTP协议等非常复杂的服务,如果没有容器来提供这些服务,我们就无法编写像Servlet这样代码简单,功能强大的组件。早期的JavaEE服务器提供的EJB容器最重要的功能就是通过声明式事务服务,使得EJB组件的开发人员不必自己编写冗长的事务处理代码,所以极大地简化了事务处理。
Spring的核心就是提供了一个IoC容器,它可以管理所有轻量级的JavaBean组件,提供的底层服务包括组件的生命周期管理、配置和组装服务、AOP支持,以及建立在AOP基础上的声明式事务服务等。
什么是IoC?IoC全称Inversion of Control,直译为控制反转。
如果一个系统有大量的组件,其生命周期和相互之间的依赖关系如果由组件自身来维护,不但大大增加了系统的复杂度,而且会导致组件之间极为紧密的耦合,继而给测试和维护带来了极大的困难。
因此,核心问题是:
- 谁负责创建组件?
- 谁负责根据依赖关系组装组件?
- 销毁时,如何按依赖顺序正确销毁?
解决这一问题的核心方案就是IoC。
- 传统的应用程序中,控制权在程序本身,程序的控制流程完全由开发者控制
- 在IoC模式下,控制权发生了反转,即从应用程序转移到了IoC容器,所有组件不再由应用程序自己创建和配置,而是由IoC容器负责,这样,应用程序只需要直接使用已经创建好并且配置好的组件。
IoC解决了一个最主要的问题:将组件的创建+配置与组件的使用相分离,并且,由IoC容器负责管理组件的生命周期。因为IoC容器要负责实例化所有的组件,因此,有必要告诉容器如何创建组件,以及各组件的依赖关系。
在Spring的IoC容器中,我们把所有组件统称为JavaBean,即配置一个组件就是配置一个Bean。
使用Spring的IoC容器,实际上就是通过类似XML这样的配置文件,把我们自己的Bean的依赖关系描述出来,然后让容器来创建并装配Bean。Spring容器会自动为每个Bean创建一个单例(Singleton),即容器初始化时创建Bean,容器关闭前销毁Bean。一旦容器初始化完毕,我们就直接从容器中获取Bean使用它们,例如:
<beans>
<bean id="dataSource" class="HikariDataSource" />
<bean id="bookService" class="BookService">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="userService" class="UserService">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
使用XML配置的优点是所有的Bean都能一目了然地列出来,并通过配置注入能直观地看到每个Bean的依赖。它的缺点是写起来非常繁琐,每增加一个组件,就必须把新的Bean配置到XML中。
我们也可以使用Annotation配置,可以完全不需要XML,让Spring自动扫描Bean并组装它们,比如:
@Component
public class MailService {
...
}
@Component
public class UserService {
@Autowired
MailService mailService;
...
}
- 这个@Component注解就相当于定义了一个Bean
- 使用@Autowired就相当于把指定类型的Bean注入到指定的字段中。和XML配置相比,@Autowired大幅简化了注入,因为它不但可以写在set()方法上,还可以直接写在字段上,甚至可以写在构造方法中。
使用Annotation配合自动扫描能大幅简化Spring的配置,我们只需要保证:
- 每个Bean被标注为@Component并正确使用@Autowired注入;
- 配置类被标注为@Configuration和@ComponentScan;
- 所有Bean均在指定包以及子包内。
5.2 Spring Boot
Spring Boot是一个基于Spring的套件,它帮我们预组装了Spring的一系列组件,以便以尽可能少的代码和配置来开发基于Spring的Java应用程序。
简而言之,我们基于Spring Boot的预置结构继续开发,更加省时省力。
比如:
@SpringBootApplication
public class Application {
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
}
启动Spring Boot应用程序只需要一行代码加上一个注解@SpringBootApplication,该注解实际上又包含了:
@SpringBootConfiguration
@Configuration
@EnableAutoConfiguration
@AutoConfigurationPackage
@ComponentScan
这样一个注解就相当于启动了自动配置和自动扫描。
事实上,Spring Boot大量使用XxxAutoConfiguration来使得许多组件被自动化配置并创建,而这些创建过程又大量使用了Spring的Conditional功能。从而让我们基于很少的配置和代码快速搭建出一个完整的应用程序。
SpringBoot核心的关键特性:
- 起步依赖(Starter Dependency)
创建项目时,会默认添加基础依赖,简化我们自己查找依赖的过程。 - 自动配置(Auto Configuration)
创建项目时,springboot工程添加的默认依赖中提供了很多默认的配置,简化了我们对资源的配置过程。 - 健康检查(Actator)
监控-springboot工程运行时,我们可以打开actuator特性,基于此特性监控spring中的bean,连接池,jvm内存等 - 嵌入式服务(Tomcat)
springboot工程支持内嵌的web服务,可以将tomcat这样的服务直接嵌套到web依赖中,简化部署过程。
总之,Spring Boot 框架就是要基于快速构建理念,基于约定大于配置方式,实现技术的开箱即用,以提高开发效率。
5.3 Spring Cloud
Spring Cloud 就是微服务系统架构的一站式解决方案,在平时我们构建微服务的过程中需要做如 服务发现注册 、配置中心 、消息总线 、负载均衡 、断路器 、数据监控 等操作,而 Spring Cloud 为我们提供了一套简易的编程模型,使我们能在 Spring Boot 的基础上轻松地实现微服务项目的构建。
以下是SpringCloud的一些核心组件介绍:
5.3.1 核心组件Eureke(服务发现)
是微服务架构中的注册中心,由Eureka Client和Eureka Server两部分组成,前者负责将各个服务的信息注册到Eureka Server中。后者为注册中心,里面有一个注册表,保存了各个服务所在的机器和端口号;
各个服务启动时,Eureka Client都会将服务注册到Eureka Server,并且Eureka Client还可以反过来从Eureka Server拉去注册表,从而知道其他服务在哪里;
5.3.2 核心组件Feign(动态代理)
使用了动态代理的机制,用注解定义一个FeignClient接口,向指定的服务建立连接、发起请求、获取响应,解析响应等等。
5.3.3 核心组件Ribbon(客户端负载均衡)
Ribbon的作用是负载均衡,当我们要请求的服务部署在多台机器上时,Feign就不知道该请求哪台机器。此时具有负载均衡作用的Ribbon就会帮你在每次请求时选择一台机器,均匀的把请求分发到各台机器上。
此外Ribbon是和Feign以及Eureka紧密协作的,具体如下:
- 首先Ribbon会从Eureka Client里获取到对应的服务注册表,知道所有的服务都部署在了哪些机器上,在监听哪些端口号。
- 然后Ribbon就使用默认的Round Robin轮询算法,从中选择一台机器;
- 最后Feign就会针对这台机器,构造并发起请求。
5.3.4 核心组件Hystrix(断路器)
Hystrix是隔离、熔断以及降级的一个框架。
发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题;
5.3.5 核心组件Zuul(服务网关)
如果前端、移动端要调用后端系统,统一从Zuul网关进入,由Zuul网关转发请求给对应的服务;