Java高级编程架构——Spring实战:Spring初探

 

主要内容

  • Spring的使命——简化Java开发
  • Spring容器
  • Spring的整体架构
  • Spring的新发展

现在的Java程序员赶上了好时候。在将近20年的历史中,Java的发展历经沉浮。尽管有很多为人诟病的产品,例如applets、EJB、Java Data Object(JDO)和数不清的日志框架,Java还是发展为一个庞大且丰富的开发平台,很多企业级应用都是基于JVM平台构建。Spring是JVM开发平台中的一颗明珠。

Spring最开始出现的目的是替代企业级开发框架EJB,相比EJB,Spring提供更轻量和更易用的编程模型。Spring的重要特点是非侵入式增强POJO(plain old java object)的能力。

在后续的发展过程中,EJB也效仿Spring的做法提供了简单的以POJO为中心的编程模型,现在的EJB框架也拥有依赖注入(DI)和面向切面编程(AOP)能力,可以论证是受Spring成功的影响。

尽管J2EE一直在追赶Spring的发展,但是Spring本身也没有停止进步。现在,Spring在一些J2EE刚刚涉入或者完全没有涉入的领域飞速发展:移动开发、社交API整合、NoSQL数据库、云计算和大数据。就目前来看,Spring的未来一片光明。

重要的事情再强调一遍:现在的Java程序员赶上了好时候。

这篇文章会从一个比较高的层次探索Spring,介绍Spring框架解决了哪些主要问题。

1.1 简化Java开发

Spring是一种开源框架,由Rod Johnson发明,并在其著作《Expert One-on-One:J2EE设计与开发》。Spring的初衷是降低企业级开发的复杂性,并试图通过POJO对象实现之前EJB这类重型框架才能实现的功能。Spring不仅仅对服务端开发有用,任何Java应用都可受益于Spring的简洁、易测试和低耦合等特性。

Spring框架中使用beans或JavaBeans来表示应用程序中的组件,但这并不意味着该组件必须严格满足Java Bean的规范。

Spring做了很多事情,但是归根到底是一些基本的思路,而所有这些思路最终都导向Spring的使命:简化Java开发

Spring通过下列四种策略来简化Java开发:

  • 基于POJO的轻量级、最小侵入式开发;
  • 通过依赖注入和面向接口编程实现松耦合;
  • 通过面向切面编程和惯例实现声明式编程;
  • 通过面向切面编程和模板消除样板式代码(boierplate code)

几乎Spring的每条特性都可以追溯到这四条策略之一,接下来分别对这四条策略进行阐述,并给出具体的代码说明Spring如何简化Java开发。

1.1.1 激发POJO的能力

如果你做Java开发足够久,你应该遇到过很多会束缚程序员能力的开发框架,这些框架要求程序员继承框架提供的类或者实现它提供的接口,例如EJB框架中的session beans,另外,在EJB之前的很多框架中也有类似的侵入式编程模型,如Struts、WebWork、Tapestry等等。

Spring尽量避免让自己的API污染你的应用代码。Spring几乎不会强制要求开发人员实现某个Spring提供的接口或者继承某个Spring提供的类,在Spring应用中的Java类看起来和普通类一样,不过,Spring现在经常使用注解来修饰Java类,但是这个类还是一个POJO。

举个代码例子说明,看如下的HelloWorldBean

package com.spring.sample;

public class HelloWorldBean {

public String sayHello() {

return "Hello World";

}

}

可以看出,这就是一个简单的Java类-POJO,没有什么特殊的标志表明它是一个Spring组件。Spring这种非侵入式编程模型使得这个类在Spring和非Spring框架下具备相同的功能。

尽管形式非常简单,POJO的能力值却可能非常高,例如Spring可以通过依赖注入编织这些POJOs来激发POJO的能力。

1.1.2 依赖注入

依赖注入听起来比较吓人,貌似一种非常复杂的编程技术或者设计模式。实际上依赖注入并不复杂,通过在工程中应用依赖注入技术,可以得到更简单、更容易理解和测试的代码。

How DI works

除了Hello-world级别的程序,稍微复杂一点的Java应用都需要多个类配合实现功能。一般而言,每个类自己负责获取它要合作的类对象的引用,这会导致代码高度耦合且难以测试。

首先看如下代码:

package com.spring.sample.knights;

public class DamselRescuingKnight implements Knight {

private RescueDamselQuest quest;

public DamselRescuingKnight() {

this.quest = new RescueDamselQuest(); //与RescueDamselQuest紧耦合

}

public void embarkOnQuest() {

quest.emark();

}

}

可以看出,DamselRescuingKnight在它的构造函数中创建了自己的Quest实例——RescueDamselQuest实例,这使得DamselRescuingKnight与RescueDamselQuest紧密耦合,如果需要刺杀Damsel,则这个刀可以使用,但是如果需要刺杀恐龙,则这个刀就派不上用场了。

更糟的是,给DamselRescuingKnight写单元测试很不方便,在这个测试中,你必须确认:当调用knight的emarkOnQuest函数时,quest的embark函数也正确调用,但这并不容易。

耦合是一头双头怪:一方面,紧耦合的代码难以测试、难以复用并且难以理解,并且经常陷入“修复一个bug但引入一个新的bug”的开发怪圈中;另一方面,应用程序必须存在适当的耦合,否则该应用无法完成任何功能。总之,耦合是必要的,但是应该控制组件之间的耦合程度。

通过使用依赖注入(DI)技术,对象之间的依赖关系由Spring框架提供的容器进行管理,而不需要某个对象主动创建自己需要的引用,如下图所示:

 

依赖注入的作用

再看一个BraveKnight类的例子:

package com.spring.sample.knights;

public class BraveKnight implements Knight {

private Quest quest;

public BraveKnight(Quest quest) { // Quest实例被注入

this.quest = quest;

}

public void embarkOnQuest() {

quest.emark();

}

}

该对象不再局限于一种quest实例,在构造过程中利用构造函数的参数传入quest实例,这种类型的依赖注入称为构造注入。

还有一点需要注意,使用接口定义quest实例,这就是面向接口编程,使得BraveKnight不再局限于某种特定的Quest实现,这就是DI带来的最大的好处——松耦合。

欢迎关注笔者,回复 ‘资料’,获取Java工程化管理、高性能、分布式、高可用架构、zookeeper、Spring cloud,MyBatis,Netty源码分析和大数据等资料。

实现依赖注入

在上述例子代码可以看出,Spring相当于将依赖注入的位置从BraveKnight类中剥离出来,那么具体的依赖注入代码如何写呢?开发人员如何规定给BraveKnight注入哪个Quest实现,例如SlayDragonQuest?

package com.spring.sample.knights;

import java.io.PrintStream;

public class SlayDragonQuest implements Quest {

private PrintStream stream;

public SlayDragonQuest(PrintStream stream) {

this.stream = stream;

}

public void emark() {

stream.println("Embarking on quest to slay the dragon!");

}

}

在Spirng框架中,最通用的方法是通过写XML配置文件来定义组件之间的依赖关系,如下所示:

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

在这个xml配置文件中分别定义了BraveKnight和SlayDragonQuest两个bean:在BraveKnightbean的定义中,通过构造器函数传入一个SlayDragonQuest的引用;在SlayDragonQuest的定义中,通过SpEL语言将System.out传入它的构造函数。

Spring 3.0引入了JavaConfig,这种写法比xml文件的好处是具备类型安全检查,例如,上面XML配置文件可以这么写:

package com.spring.sample.knights.config;

import com.spring.sample.knights.BraveKnight;

import com.spring.sample.knights.Knight;

import com.spring.sample.knights.Quest;

import com.spring.sample.knights.SlayDragonQuest;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

@Configuration

public class KnightConfig {

@Bean

public Knight knight() {

return new BraveKnight(quest());

}

@Bean

public Quest quest() {

return new SlayDragonQuest(System.out);

}

}

不论是基于XML的配置还是基于Java文件的配置,都由Spring框架负责管理beans之间的依赖关系。

启动依赖注入

在Spring应用中,由application context负责加载beans,并将这些beans根据配置文件编织在一起。Spring框架提供了几种application context的实现,如果使用XML格式的配置文件,则使用ClassPathXmlApplicationContext;如果使用Java文件形式的配置文件,则使用AnnotationConfigApplicationContext。

package com.spring.sample.knights;

import com.spring.sample.knights.config.KnightConfig;

import org.springframework.context.ApplicationContext;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class KnightMain {

public static void main(String[] args) {

// ClassPathXmlApplicationContext context =

// new ClassPathXmlApplicationContext("classpath:/knight.xml");

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(KnightConfig.class);

Knight knight = context.getBean(Knight.class);

knight.embarkOnQuest();

context.close();

}

}

上述代码中,根据KnightConfig.java文件创建Spring应用上下文,可以把该应用上下文看成对象工厂,来获取idknight的bean。

如果你想了解更多关于DI的知识,可以查看Dhanji R. Prasanna's Dependency Injectionhttps://www.manning.com/books/dependency-injection一书。

1.1.3 切面编程

依赖注入(DI)实现了模块之间的松耦合,而利用面向切面编程(AOP)可以将涉及整个应用的基础功能(安全、日志)放在一个可复用的模块中。

AOP是一种在软件系统中实现关注点分离的技术。软件系统由几个模块构成,每个模块负责一种功能,不过在系统中有些需求需要涉及到所有的模块,例如日志、事务管理和安全等。如果将这些需求相关的代码都分散在各个模块中,一方面是不方便维护、另一方面是与原来每个模块的业务逻辑代码混淆在一起,不符合单一职责原则。

  • 实现系统级别处理的代码分散在多个子模块中,这意味着如果要修改这些处理代码,则要在每个模块中都进行修改。即使将这些代码封装到一个模块中,在没给个子模块中只保留对方法的调用,这些方法调用还是在各个模块中重复出现。
  • 业务逻辑代码与非核心功能的代码混淆在一起。例如,一个添加address book的方法应该只关心如何添加address book,而不应该关心该操作是否安全或者是否能够实现事务处理。

下面这张图可以体现这种复杂性,左边的业务逻辑模块与右边的系统服务模块沟通太过密切,每个业务模块需要自己负责调用这些系统服务模块。

 

业务逻辑模块与系统服务模块过度交互

AOP可以模块化这些系统服务,然后利用声明式编程定义该模块需要应用到那些业务逻辑模块上。这使得业务模块更简洁,更专注于处理业务逻辑,简而言之,切面(aspects)确保POJO仍然是普通的Java类。

可以将切面想象为覆盖在一些业务模块上的毯子,如下图所示。在系统中有一些模块负责核心的业务逻辑,利用AOP可以为所有这些模块增加额外的功能,而且核心业务模块无需知道切面模块的存在。

 

切面就像毯子一样覆盖在几个核心业务模块之上

AOP实践

继续上面的例子,如果需要一个人记录BraveKnight的所作所为,下面代码是该日志服务:

package com.spring.sample.knights;

import java.io.PrintStream;

public class Minstrel {

private PrintStream stream;

public Minstrel(PrintStream stream) {

this.stream = stream;

}

public void singBeforeQuest() {

stream.println("Fa la la, the knight is so brave!");

}

public void singAfterQuest() {

stream.println("Tee hee hee, the brave knight did embark on a quest!");

}

}

然后在XML文件中定义Minstrel对应的切面:

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance xmlns:aop="http://www.springframework.org/schema/aop"

xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

在这个配置文件中增加了aop配置名字空间。首先定义Minstrel的bean,然后利用标签定义aop相关的配置;然后在节点中引用minstrel,定义方面;aspect负责将pointcut和要执行的函数(before、after或者around)连接在一起。

还有一种更先进的写法,利用注解和Java配置文件,可以参考aop docs

Spring框架中的一些子模块也是基于AOP实现的,例如负责事务处理和负责安全的模块。

1.1.4 使用模板消除重复代码

在编程过程中有没有感觉经常需要写重复无用的代码才能实现简单的功能,最经典的例子是JDBC的使用,这些代码就是样板式代码(boilerplate code)。

以JDBC的使用举个例子,这种原始的写法你一定见过:

public Employee getEmployeeById(long id) {

Connection conn = null;

PreparedStatement stmt = null;

ResultSet rs = null;

try {

conn = dataSource.getConnection();

stmt = conn.prepareStatement("select id, name from employee where id=?");

stmt.setLong(1, id);

rs = stmt.executeQuery();

Employee employee = null;

if (rs.next()) {

employee = new Employee();

employee.setId(rs.getLong("id"));

employee.setName(rs.getString("name"));

}

return employee;

} catch (SQLException e) {

} finally {

if (rs != null) {

try {

rs.close();

} catch (SQLException e) {

}

}

if (stmt != null) {

try {

stmt.close();

} catch (SQLException e) {

}

}

if (conn != null) {

try {

conn.close();

} catch (SQLException e) {

}

}

}

return null;

}

可以看到,上面这么一坨代码中只有少数是真正用于查询数据(业务逻辑)的。除了JDBC的接口,其他JMS、JNDI以及REST服务的客户端API等也有类似的情况出现。

Spring试图通过模板来消除重复代码,这里所用的是模板设计模式。对于JDBC接口,Spring提供了JdbcTemplate模板来消除上面那个代码片段中的样板式代码,例子代码如下:

public Employee getEmployeeById(long id) {

return jdbcTemplate.queryForObject(

"select id, name from employee where id=?",

new RowMapper() {

public Employee mapRow(ResultSet resultSet, int i) throws SQLException {

Employee employee = new Employee();

employee.setId(resultSet.getLong("id"));

employee.setName(resultSet.getString("name"));

return employee;

}

});

}

你没有看错,就是利用回调函数实现的,有兴趣的读者可以深入研究下JdbcTemplate的源码实现。

我们上面已经演示了Spring简化Java开发的四种策略:面向POJO开发、依赖注入(DI)、面向切面编程和模板工具。在举例的过程中,我们稍微提到一点如何使用XML配置文件定义bean和AOP相关的对象,但是这些配置文件的加载原理是怎样的?这就需要研究下Spring的容器,Spring中所定义的bean都由Spring容器管理。

1.2 使用容器管理beans

基于Spring框架构建的应用中的对象,都由Spring容器(container)管理,如下图所示。Spring容器负责创建对象、编织对象和配置对象,负责对象的整个生命周期。

 

Spring容器的作用

容器是Spring框架的核心,通过依赖注入(DI)管理构成Spring应用的组件。正是因为有容器管理各个组件之间的协作关系,使得每个Spring组件都很好理解、便于复用和单元测试。

Spring容器有多种实现,可以分为两类:

  • Bean factories(由org.springframework.beans.factory.BeanFactory接口定义)是最简单的容器,只提供基本的依赖注入功能;
  • Application context(由org.springframework.context.ApplicationContext接口定义)在bean factory的基础上提供application-framework框架服务,例如可以从properties文件中解析配置信息、可以对外公布application events。

1.2.1 应用上下文(application context)

Spring提供了多种application context,可列举如下:

  • AnnotationConfigApplicationContext——从Java配置文件中加载应用上下文;
  • AnnotationConfigWebApplicationContext——从Java配置文件中加载Spring web应用上下文;
  • ClassPathXmlApplicationContext——从classpath(resources目录)下加载XML格式的应用上下文定义文件;
  • FileSystemXmlApplicationContext——从指定文件系统目录下加载XML格式的应用上下文定义文件;
  • XmlWebApplicationContext——从classpath(resources目录)下加载XML格式的Spring web应用上下文。

通过应用上下文实例,可以通过getBean()方法获得对应的bean。

1.2.2 bean的生命周期

在传统的Java应用中,一个对象的生命周期非常简单:通过new创建一个对象,然后该对象就可以使用,当这个对象不再使用时,由Java垃圾回收机制进行处理和回收。

在Spring应用中,bean的生命周期的控制更加精细。Spring提供了很多节点供开发人员定制某个bean的创建过程,掌握这些节点如何使用非常重要。Spring中bean的生命周期如下图所示:

 

bean的生命周期

可以看出,bean factory负责bean创建的最初四步,然后移交给应用上下文做后续创建过程:

  1. Spring初始化bean
  2. Spring将值和其他bean的引用注入(inject)到当前bean的对应属性中;
  3. 如果Bean实现了BeanNameAware接口,Spring会传入bean的ID来调用setBeanName方法;
  4. 如果Bean实现了BeanFactoryAware接口,Spring传入bean factory的引用来调用setBeanFactory方法;
  5. 如果Bean实现了ApplicationContextAware接口,Spring将传入应用上下文的引用来调用setApplicationContext方法;
  6. 如果Bean实现了BeanPostProcessor接口,则Spring调用postProcessBeforeInitialization方法,这个方法在初始化和属性注入之后调用,在任何初始化代码之前调用;
  7. 如果Bean实现了InitializingBean接口,则需要调用该接口的afterPropertiesSet方法;如果在bean定义的时候设置了init-method属性,则需要调用该属性指定的初始化方法;
  8. 如果Bean实现了BeanPostProcessor接口,则Spring调用postProcessAfterInitialization方法
  9. 在这个时候bean就可以用于在应用上下文中使用了,当上下文退出时bean也会被销毁;
  10. 如果Bean实现了DisposableBean接口,Spring会调用destroy()方法;如果在bean定义的时候设置了destroy-method, 则此时需要调用指定的方法。

本节主要总结了如何启动Spring容器,以及Spring应用中bean的生命周期。

1.3 Spring整体架构

除了Spring的核心模块,Spring还提供了其他的工具组件,这些组件扩展了Spring的功能,例如webservice、REST、mobile和NOSQL,形成了丰富的开发生态。

1.3.1 Spring模块

Spring 4.0you 20个独立的模块,每个包含三个文件:二进制库、源文件和文档,完整的库列表如下图所示:

 

Spring 4.0包含20个模块

按照功能划分,这些模块可以分成六组,如下图所示:

 

Spring框架的六组模块

这些模块几乎可以满足所有企业级应用开发的需求,但是开发人员并不需要完全使用Spring的这些模块,可以自由选择符合项目需求的第三方模块——Spring为一些第三方模块提供了交互接口。

CORE SPRING CONTAINER

Spring框架的核心模块,其他所有模块都基于该模块构建。Spring容器负责管理Spring应用中bean的创建、配置和管理。在这模块中有Spring bean factory,该接口提供了最基本的依赖注入(DI)功能;基于bean factory,该模块提供了集中Spring应用上下文的实现,可以供开发人员选择。

除了bean factory和application context,该模块还支持其他企业级服务,例如email、JNDI access、EJB integration和scheduling。

SPRING's AOP MODULE

Spring框架通过AOP模块提供面向切面编程的能力。通过AOP模块,一些系统层面的需求(事务、安全)可以与它们真正要作用到的模块相互解耦合。

DATA ACCESS AND INTEGRATION

Spring的JDBC和data-access object模块将数据库操作的一些样板式代码封装起来,免去了开发人员的很多工作量。这个模块还对数据库层的异常进行了封装,并向上提供含义更丰富的异常信息。

Spring并未实现自己的ORM框架,但是它提供了跟其他几个ORM框架整合的能力,例如Hibernate、Mybatis、Java Persistence AP等等,而且这些ORM框架都支持使用Spring提供的事务管理模块。

WEB AND REMOTING

Spring提供了自己的 WEB开发框架——Spring MVC,除此之外,这个模块还提供远程调用支持:Remote Method Invocation(RMI)、Hessian、Burlap和JAX-WS。

INSTRUMENTATION

不常使用

TESTING

可以与常用的JUNIT、Mockito、Spock等测试框架整合使用。

1.3.2 Spring portfolio

如果只是学习Spring的核心模块,将会错过不少Spring社区提供的经典项目,下面介绍的这些项目使得Spring几乎可以覆盖整个Java开发(PS:带*的项目值得每位Spring用户仔细学习)。

SPRING WEB FLOW

基于Spring MVC框架拓展,利用该框架可以构建流式web应用。

SPRING WEB SERVICE

虽然核心的Spring 框架提供了将Spring Bean 以声明的方式发布为Web Service,但是这些服务基于一个具有争议性的架构(拙劣的契约置后模型)之上而构建的。这些服务的契约由Bean 的接口来决定。 Spring Web Service 提供了契约优先的Web Service模型,服务的实现都是为了满足服务的契约而编写的。

SPRING SECURITY(*)

安全对于许多应用都是一个非常关键的切面。利用Spring AOP,Spring Security为Spring 应用提供了声明式的安全机制。我们将在第9 章讲解如何为应用添加SpringSecurity。你可以在主页http://static.springsource.org/spring-security/site 获得关于SpringSecurity 更多的信息。

SPRING INTEGRATION

许多企业级应用都需要与其他应用进行交互。Spring Integration 提供了几种通用的应用集成模式的Spring 声明式风格的实现。

我们不会在本书覆盖Spring Integration 内容,但是如果你想了解更多关于SpringIntegration 的信息, 我推荐Mark Fisher、Jonas Partner、Marius Bogoevici 和IweinFuld 编写的《Spring Integration in Action》;或者还可以访问Spring Integration 的主页http://www.springsource.org/spring-integration。

SPRING BATCH

当我们需要对数据进行大量操作时,没有任何技术可以比批处理更能胜任此场景的。如果需要开发一个批处理应用,你可以借助于Spring 强大的面向POJO 的编程模型来使用Spring Batch 来实现。

Spring Batch 超出了本书的范畴,但是你可以阅读Thierry Templier 和Arnaud Cogoluègnes编写的《Spring Batch in Action》,或者访问Spring Batch 的主页http://static.springsource.org/spring-batch。

SPRING DATA(*)

Spring Data用于简化数据库相关的开发工作。尽管多年以来关系型数据库都是企业级应用开发的主流,但是随着移动互联网的发展,对NoSQL这类菲关系型数据库的需求也越来越强。

无论你选择NoSQL还是关系型数据库,Spring Datat都能提供简洁的编程模型,例如非常方便的repository机制,可以为开发人员自动创建具体的SQL实现。

SPRING SOCIAL

社交网络是互联网冉冉升起的一颗新星,越来越多的应用正在融入社交网络网站,例如Facebook 或者Twitter。如果对此感兴趣,你可以了解下Spring Social,Spring 的一个社交网络扩展模块。

Spring Social 相对还比较新颖,我并没有计划将它放入本书,但是你可以访问http://www.springsource.org/spring-social 了解Spring Social 更多的相关信息。

SPRING MOBILE

移动应用是另一个引人瞩目的软件开发领域。智能手机和平板设备已成为许多用户首选的客户端。Spring Mobile 是Spring 新的扩展模块用于支持移动Web 应用开发。

与Spring Mobile 相关的是Spring Android 项目。这个新项目旨在通过Spring 框架为开发基于Android 设备的本地应用提供某些简单的支持。最初,这个项目提供了Spring 的RestTemplate 版本(请查看第11 章了解RestTemplete)可以用于Android 应用。

再次声明,这两个项目已超出了本书的范围,但是如果你对这两个项目感兴趣,可以访问http://www.springsource.org/spring-mobile 和http://www.springsource.org/spring-android 了解更多相关的信息。

SPRING BOOT(*)

Spring Boot是Spring社区中发展速度最快的框架之一,它旨在简化Spring的使用,解决Spring开发时遇到的“配置地狱”问题。

Spring Boot通过大量使用自动配置技术,可以取消大量的XML配置文件,同时该框架提出了starter的概念,用于简化pom文件。可以参考我的一系列博文:《Spring Boot Cookbook》阅读笔记

1.4 Spring的新特点(书中总结和自己的观点)

主要总结下Spring社区的趋势:

  1. 注重注解,能用注解解决的尽量用注解,尽量少写XML配置文件;
  2. Spring Boot已经是Spring社区中增长最迅速的框架,前三名是:Spring Framework,Spring Boot和Spring Security
  3. 支持Java 8,通过Java8的lambda表达式,使得一些回调接口更易使用和阅读。
  4. 与Groovy开发平滑支持,Groovy是JVM上的Python语言,在Spring项目中可以写单元测试;
  5. 支持JSR-310:Data和Time API,为开发人员提供丰富的接口操作java.util.Date或者java.util.Clendar。

欢迎工作一到五年的Java工程师朋友们加入Java架构开发:468947140

点击链接加入群聊【Java-BATJ企业级资深架构】:https://jq.qq.com/?_wv=1027&k=52j2FVO

本群提供免费的学习指导 架构资料 以及免费的解答

不懂得问题都可以在本群提出来 之后还会有职业生涯规划以及面试指导

你可能感兴趣的:(java,数据库,大数据)