1.Spring框架
1.1.概述
Spring是一个支持快速开发Java EE应用程序的框架,它提供了一系列底层容器和基础设施,并可以和大量常用的开源框架无缝集成,Spring Framework主要包括几个模块:
- 支持IoC和AOP的容器;
- 支持JDBC和ORM的数据访问模块;
- 支持声明式事务的模块;
- 支持基于Servlet的MVC开发;
- 支持基于Reactive的Web开发;
2.IOC容器
2.1.什么是IOC容器
什么是容器?容器是一种为某种特定组件的运行提供必要支持的一个软件环境。例如,Tomcat就是一个Servlet容器,它可以为Servlet的运行提供运行环境。类似Docker这样的软件也是一个容器,它提供了必要的Linux环境以便运行一个特定的Linux进程。
Spring的核心就是提供了一个IoC容器,它可以管理所有轻量级的JavaBean组件,提供的底层服务包括组件的生命周期管理、配置和组装服务、AOP支持,以及建立在AOP基础上的声明式事务服务等。
什么是IOC?IoC全称Inversion of Control,直译为控制反转
在传统的应用程序中,控制权在程序本身,程序的控制流程完全由开发者控制,例如:我们的服务层调用数据层时,首先我们需要在服务层层通过new创建出数据层对象,然后再调用数据层对象得到数据,总结来说就是,所有的资源实例都是由我们自己的APP直接创建和控制资源实例,然后也是通过APP直接调用资源实例。

在ioc容器控制的程序中,我们可以通过将所有对象统一创建放入IOC容器中,然后什么地方需要实例对象时在从ioc容器中获取对应的实例对象,还是刚才那个例子,当我们需要在服务层调用数据层时,我们不需要直接创建数据层实例对象,而是通过从IOC容器中获取数据层实例,这样我们就从主动控制对象实例创建变成了主动获取实例对象,在IOC容器帮助下不需要我们主动去控制创建对象实例的创建,我们将主动控制创建实例的任务交由让ioc容器帮我们完成,这个过程就叫做控制反转(IOC)。总的来说就是所有的资源实例交由IOC容器来创建和控制,然后我们再去IOC容器中获取资源实例。

Spring的IoC容器是一个高度可扩展的无侵入容器。所谓无侵入,是指应用程序的组件无需实现Spring的特定接口,或者说,组件根本不知道自己在Spring的容器中运行。这种无侵入的设计有以下好处:
- 应用程序组件既可以在Spring的IoC容器中运行,也可以自己编写代码自行组装配置;
- 测试的时候并不依赖Spring容器,可单独进行测试,大大提高了开发效率。
2.2.装配Bean(实例化bean)
IOC容器装配Bean就是通过ioc容器加载所有的资源,将其保存在IOC容器中。ioc容器加载了哪些资源可以通过xml或者注解两种方式进行配置。
下面通过一个用户登录例子完成IOC容器装配Bean
结构如下:

User实体类,数据库中表对应的实体类
public class User {
private String id;
private String name;
private String password;
public User(String id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", password='" + password + '\'' +
'}';
}
}
UserMapper数据层,该类用于模拟真实的数据库数据返回
public class UserMapper {
private List<User> users = new ArrayList<>(Arrays.asList( // users:
new User("1", "Bob", "password"), // bob
new User("2", "Alice", "password"), // alice
new User("3", "Tom", "password"))); // tom
public User getUserById(String id){
List<User> collect = users.stream().filter(user -> user.getId().equals(id)).collect(Collectors.toList());
return collect.get(0);
}
public List<User> getAll(){
return this.users;
}
public Integer addUser(User user){
this.users.add(user);
return 1;
}
}
UserService服务层,用于处理业务逻辑,调用数据层进行数据持久化。
public class UserService {
UserMapper userMapper;
public User login(String name, String password) {
List<User> users=userMapper.getAll();
for (User user : users) {
if (user.getPassword().equalsIgnoreCase(password) && user.getName().equals(name)) {
System.out.println("登录成功!");
return user;
}
}
throw new RuntimeException("登录失败!");
}
public User getUser(String id) {
return userMapper.getUserById(id);
}
public Integer register(String name,String password) {
UUID uuid = UUID.randomUUID();
return userMapper.addUser(new User(uuid.toString(),name,password));
}
}public class UserService {
UserMapper userMapper;
public void setUserMapper(UserMapper userMapper){
this.userMapper=userMapper;
}
public User login(String name, String password) {
List<User> users=userMapper.getAll();
for (User user : users) {
if (user.getPassword().equalsIgnoreCase(password) && user.getName().equals(name)) {
System.out.println("登录成功!");
return user;
}
}
throw new RuntimeException("登录失败!");
}
public User getUser(String id) {
return userMapper.getUserById(id);
}
public Integer register(String name,String password) {
UUID uuid = UUID.randomUUID();
return userMapper.addUser(new User(uuid.toString(),name,password));
}
}
APP启动类,用于创建ioc容器和加载bean
public class App {
public static void main(String[] args) {
//创建ioc容器并加载和控制bean
}
}
如何将上面的登录案例转换为由IOC容器控制呢?可以通过xml或者注解这两种方式进行配置。
首先加入Spring框架的maven依赖:spring-context,我这里用的5.2.8版本
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>Spring-Learn</groupId>
<artifactId>Spring-Learn</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.8.RELEASE</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
2.2.1.xml方式装配Bean
通过xml文件可以清晰描述Bean的依赖关系。
在resources目录下创建application.xml文件,内容如下
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userMapper" class="gc.learn.mapper.UserMapper">
</bean>
<bean id="userService" class="gc.learn.service.UserService">
<property name="userMapper" ref="userMapper"></property>
</bean>
</beans>
下面的bean配置表示实例化一个bean,class是将gc.learn.mapper包下的UserMapper类交由IOC容器管理,默认情况下该方式是直接通过无参数的构造方法创建的实例(如果该类没有无参数构造方法将无法创建,需要通过在bean标签中添加constructor-arg标签进行构造函数中参数的依赖注入),id是该类的唯一身份标识,后面从IOC容器中获取UserMapper类实例的时候需要用到该id值。
<bean id="userMapper" class="gc.learn.mapper.UserMapper">
<!--为有参的构造函数注入参数-->
<!--<constructor-arg name="" value=""></constructor-arg>-->
</bean>
下面的bean配置表示实例化一个bean并依赖注入了参数,将gc.learn.service包下的UserService类交由IOC容器管理(实例化),然后通过property标签依赖注入成员变量userMapper的值为IOC容器中对应的bean实例,该实例通过id为userMapper来获取,注意:property标签中的name属性值为成员变量的属性名,ref属性值为IOC容器管理中存在的id对象实例,value属性用于设置基础的数据类型(String、int、Integer、boolean、Boolean、char)的值,property标签是通过调用类中的set方法实现的,所以要通过property标签设置值或者对象实例,该类必须写一个set方法。
<bean id="userService" class="gc.learn.service.UserService">
<property name="userMapper" ref="userMapper"></property>
<property name="userServiceDescription" value="该类用于处理用户相关逻辑!"/>
</bean>
创建ioc容器并装配Bean
在APP中添加以下内容,其中ClassPathXmlApplicationContext类用于加载解析application.xml,它可以将我们配置的bean转载进ioc容器中,我们可以通过ApplicationContext应用上下文的getBean方法将对象实例id传入,得到id对应的实例,然后就可以使用该实例的方法完成用户服务的相关逻辑处理。
public class App {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
UserService userService = context.getBean("userService", UserService.class);
User loginUser = userService.login("Bob", "password");
System.out.println(loginUser);
System.out.println(userService.getUserServiceDescription());
}
}
除了构造函数实例化bean外,还有通过静态工厂方法和是实例工厂方法来进行实例化bean
静态工厂方法实例化bean
第三方的bean创建经常会使用静态工厂方法来实例bean
public class ClientService {
private static ClientService clientService = new ClientService();
private ClientService() {}
public static ClientService createInstance() {
return clientService;
}
}
<bean id="clientService"
class="examples.ClientService"
factory-method="createInstance"/>
实例工厂方法实例化bean
第三方的bean创建经常会使用静态工厂方法来实例bean
public class DefaultServiceLocator {
private static ClientService clientService = new ClientServiceImpl();
public ClientService createClientServiceInstance() {
return clientService;
}
}
<!-- 必须先实例化工厂类 -->
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
<!-- 可以通过方法注入(构造参数、方法参数等) -->
</bean>
<!-- 实例工厂方法实例化bean,通过factory-bean属性指定实例工厂类 -->
<bean id="clientService"
factory-bean="serviceLocator"
factory-method="createClientServiceInstance"/>
总结:XML这种配置方式,就是把我们自己的Bean的依赖关系描述出来,然后交由ioc容器来创建并装配Bean,实例化bean有构造函数、静态工厂方法、实例工厂方法三种方式,参数依赖注入有基于构造参数注入和setter方法参数注入两种。
2.2.2.注解方式装配Bean
使用XML配置的优点是所有的Bean都能一目了然地列出来,并通过配置注入能直观地看到每个Bean的依赖。它的缺点是写起来非常繁琐,每增加一个组件,就必须把新的Bean配置到XML中,为了避免这种繁琐操作,我们可以使用注解方式来让ioc容器装配Bean。
还是通过刚才的那个例子,我们将xml方式装配改为注解方式装配。
添加注解,描述Bean的依赖关系
因为改用了注解方式装配bean,就可以不用xml配置文件了,使用注解@Component来告诉ioc容器该类是需要被装配的,使用@Autowired来获取ioc容器中的被装配的对象实例。
UserMapper数据层,通过@Component注解将UserMapper类标记为需要被ioc容器装配管理,它id默认为userMapper,当然也可以在@Component中传入一个参数来自定义id名称。
@Component //@Component("myUserMapper")
public class UserMapper {
private List<User> users = new ArrayList<>(Arrays.asList( // users:
new User("1", "Bob", "password"), // bob
new User("2", "Alice", "password"), // alice
new User("3", "Tom", "password"))); // tom
public User getUserById(String id){
List<User> collect = users.stream().filter(user -> user.getId().equals(id)).collect(Collectors.toList());
return collect.get(0);
}
public List<User> getAll(){
return this.users;
}
public Integer addUser(User user){
this.users.add(user);
return 1;
}
}
UserService服务层,通过@Component注解将UserService类标记为需要被ioc容器装配管理,然后通过**@Autowired**获取ioc容器中的UserMapper实例并依赖注入到成员参数userMapper中,@Autowired注解可以作用于成员变量上,也可以作用于构造函数参数和成员方法参数上,@Autowired注解本质上就是xml配置中的依赖注入。
注意:@Autowired是通过类型进行注入的,ioc容器中没有该类型的对象实例时就会报错,当ioc容器中有多个对象实例相匹配时,就按照id来匹配。如果需要直接通过id进行获取可以一起配置@Autowired和@Qualifier注解使用。如果不想使用@Autowired类型方式获取对象实例,还可以单独使用@Resource注解通过id来获取对象实例。
@Component
public class UserService {
@Autowired
//@Qualifier("myUserMapper")
//@Resource(name = "myUserMapper")
private UserMapper userMapper;
private String UserServiceDescription;
public void setUserMapper(UserMapper userMapper){
this.userMapper=userMapper;
}
public void setUserServiceDescription(String s){
this.UserServiceDescription=s;
}
public User login(String name, String password) {
List<User> users=userMapper.getAll();
for (User user : users) {
if (user.getPassword().equalsIgnoreCase(password) && user.getName().equals(name)) {
System.out.println("登录成功!");
return user;
}
}
throw new RuntimeException("登录失败!");
}
public User getUser(String id) {
return userMapper.getUserById(id);
}
public Integer register(String name,String password) {
UUID uuid = UUID.randomUUID();
return userMapper.addUser(new User(uuid.toString(),name,password));
}
public String getUserServiceDescription(){
return this.UserServiceDescription;
}
}
创建ioc容器并装配Bean
通过AnnotationConfigApplicationContext方法加载配置类并创建ioc容器,因为AnnotationConfigApplicationContext只会识别带@Configuration注解的类,所以该类的必须添加上@Configuration注解,而@ComponentScan类用于扫描需要被ioc容器管理的类,所有添加了@Component注解的类都会被装配进ioc容器中。
注意:@ComponentScan不传包参数时,默认扫描添加了@ComponentScan类所在的包及其所在包下的子包。
@Configuration
@ComponentScan("gc.learn.*")
public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(App.class);
UserService userService = context.getBean(UserService.class);
User user = userService.login("Bob", "password");
System.out.println(user);
}
}
@ComponentScan注解等同下面这种配置xml扫描包形式,通过配置包扫描标签可以实现xml和注解的混合使用,混合使用时创建ioc容器必须使用xml形式进行加载。
<beans>
<context:component-scan base-package="com.acme"/>
</beans>
上面例子中的注解可以完成大部分bean的装配,但是并不能完成第三方库的装配,如果需要让IOC容器装配第三方bean应该怎么做呢(第三方bean不允许我们修改,也就不能直接添加@Component)?,答案是:通过@bean注解来实现
@bean注解,它只能作用于成员方法上,且该类必须被IOC容器管理(需要添加上@Component或者@Configuration),@bean注解作用就是将返回的参数装配进ioc容器中。
@Configuration
public class AppConfig {
@Bean
public TransferServiceImpl transferService() {
return new TransferServiceImpl();
}
}
上面的配置完全等同于下面的Spring XML。
<beans>
<bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>
2.3.注入配置(读取配置文件)
2.3.1基本数据类型配置
在开发应用程序时,经常需要读取配置文件。最常用的配置方法是以key=value的形式写在.properties文件中。
例如,MailService根据配置的app.zone=Asia/Shanghai来决定使用哪个时区,这时就需要读取配置文件,Spring容器提供了一个更简单的@PropertySource来自动读取配置文件。我们只需要在@Configuration配置类上再添加一个注解:
@Configuration
@ComponentScan
@PropertySource(value = "classpath:app.properties",encoding = "utf-8") // 表示读取classpath的app.properties
public class AppConfig {
@Value("${app.zone}")
String zoneId;
@Bean
ZoneId createZoneId() {
return ZoneId.of(zoneId);
}
}
resource目录下的app.properties文件内容如下
app.zone=Asia/Shanghai
还可以把注入的注解写到方法参数中:
@Bean
ZoneId createZoneId(@Value("${app.zone}") String zoneId) {
return ZoneId.of(zoneId);
}
另一种注入配置的方式是先通过一个简单的JavaBean持有所有的配置,例如,一个SmtpConfig:
@Component
public class SmtpConfig {
@Value("${smtp.host}")
private String host;
@Value("${smtp.port:25}")
private int port;
public String getHost() {
return host;
}
public int getPort() {
return port;
}
}
然后,在需要读取的地方,使用#{smtpConfig.host}注入:
@Component
public class MailService {
@Value("#{smtpConfig.host}")
private String smtpHost;
@Value("#{smtpConfig.port}")
private int smtpPort;
}
注意观察#{}这种注入语法,它和${key}不同的是,#{}表示从JavaBean读取属性。"#{smtpConfig.host}"的意思是,从名称为smtpConfig的Bean读取host属性,即调用getHost()方法。
2.3.2.文件资源注入
在Java程序中,我们经常会读取配置文件、资源文件等,例如,AppService需要读取logo.txt这个文件,通常情况下,我们需要写很多繁琐的代码,主要是为了定位文件,打开InputStream。
Spring提供了一个org.springframework.core.io.Resource(注意不是jarkata.annotation.Resource或javax.annotation.Resource),它可以像String、int一样使用@Value注入:
@Component
public class AppService {
@Value("classpath:/logo.txt")
private Resource resource;
private String logo;
@PostConstruct
public void init() throws IOException {
try (var reader = new BufferedReader(
new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
this.logo = reader.lines().collect(Collectors.joining("\n"));
}
}
}
注入Resource最常用的方式是通过classpath,即类似classpath:/logo.txt表示在classpath中搜索logo.txt文件,然后,我们直接调用Resource.getInputStream()就可以获取到输入流,避免了自己搜索文件的代码。
也可以直接指定文件的路径,例如:
@Value("file:/path/to/logo.txt")
private Resource resource;
2.4.装配自定义Bean
对于Spring容器来说,当我们把一个Bean标记为@Component后,它就会默认为我们创建一个单例(Singleton)对象,即ioc容器初始化时创建单例(Singleton)对象,容器关闭前销毁Bean。在容器运行期间,我们调用getBean(Class)获取到的Bean总是同一个实例(内存地址相同)。
如果我们不想创建默认的单例(Singleton)对象,可以使用**@Scope注解来配置创建Prototype(原型/多例)对象**,Prototype(原型)对象ioc容器初始化时不会创建Prototype(原型)对象,而是我们调用getBean(Class)获取到的Bean时才创建,回去到的不是同一个实例(内存地址不同)
2.4.1.配置bean模式
@Scope注解,用于指定bean类型是单例模式还是多例模式,作用于类和方法。
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // @Scope("prototype")
public class MailSession {
...
}
2.4.2.配置bean初始化和销毁时回调方法
有些时候,一个Bean在注入必要的依赖后,需要进行初始化(监听消息等)。在容器关闭时,有时候还需要清理资源(关闭连接池等)。我们通常会定义一个init()方法进行初始化,定义一个shutdown()方法进行清理。
在Bean的初始化和清理方法上标记@PostConstruct和@PreDestroy:
@Component
public class MailService {
@Autowired(required = false)
ZoneId zoneId = ZoneId.systemDefault();
@PostConstruct
public void init() {
System.out.println("Init mail service with zoneId = " + this.zoneId);
}
@PreDestroy
public void shutdown() {
System.out.println("Shutdown mail service");
}
}
使用FactoryBean
我们在设计模式的工厂方法中讲到,很多时候,可以通过工厂模式创建对象。Spring也提供了工厂模式,允许定义一个工厂,然后由工厂创建真正的Bean。
用工厂模式创建Bean需要实现FactoryBean接口。我们观察下面的代码:
@Component
public class ZoneIdFactoryBean implements FactoryBean<ZoneId> {
String zone = "Z";
@Override
public ZoneId getObject() throws Exception {
return ZoneId.of(zone);
}
@Override
public Class<?> getObjectType() {
return ZoneId.class;
}
}
当一个Bean实现了FactoryBean接口后,Spring会先实例化这个工厂,然后调用getObject()创建真正的Bean。getObjectType()可以指定创建的Bean的类型,因为指定类型不一定与实际类型一致,可以是接口或抽象类。
因此,如果定义了一个FactoryBean,要注意Spring创建的Bean实际上是这个FactoryBean的getObject()方法返回的Bean。为了和普通Bean区分,我们通常都以XxxFactoryBean命名。
2.4.3.控制bean加载顺序
@DependsOn和@Order注解
上面两个注解都可以实现对bean装配顺序的控制,不同在于@DependsOn作用于类和方法上,而@Order注解主要作用于类之上上。
例如Service层依赖于Mapper层,就可以使用下面注解进行1控制
@Component
@DependsOn("userMapper")
@Order(1)
public class UserService {
//.....
}
2.4.4.根据开发环境控制bean加载
开发应用程序时,我们会使用开发环境,例如,使用内存数据库以便快速启动。而运行在生产环境时,我们会使用生产环境,例如,使用MySQL数据库。如果应用程序可以根据自身的环境做一些适配,无疑会更加灵活。
Spring为应用程序准备了Profile这一概念,用来表示不同的环境。例如,我们分别定义开发、测试和生产这3个环境:
- native
- test
- production
创建某个Bean时,Spring容器可以根据注解@Profile来决定是否创建。例如,以下配置:
@Configuration
@ComponentScan
public class AppConfig {
@Bean
@Profile("!test") //不为测试环境时加载
ZoneId createZoneId() {
return ZoneId.systemDefault();
}
@Bean
@Profile("test") //测试环境下加载
ZoneId createZoneIdForTest() {
return ZoneId.of("America/New_York");
}
}
在运行程序时,加上JVM参数-Dspring.profiles.active=test就可以指定以test环境启动。
2.4.5.根据条件来加载bean
@Conditional注解
@Conditional会根据自定义条件判断是否加载bean,具体判断逻辑还需要我们自己实现。
例如,我们对SmtpMailService添加如下注解:
@Component
@Conditional(OnSmtpEnvCondition.class)
public class SmtpMailService implements MailService {
...
}
它的意思是,如果满足OnSmtpEnvCondition的条件,才会创建SmtpMailService这个Bean。OnSmtpEnvCondition的条件是什么呢?我们看一下代码:
public class OnSmtpEnvCondition implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return "true".equalsIgnoreCase(System.getenv("smtp"));
}
}
因此,OnSmtpEnvCondition的条件是存在环境变量smtp,值为true。这样,我们就可以通过环境变量来控制是否创建SmtpMailService。
@Conditional注解的衍生注解
@ConditionalOnProperty
它可以判断配置文件中是否包含默写选项来判断是否加载bean
例子:如果配置文件中存在app.smtp=true,则创建MailService:
@Component
@ConditionalOnProperty(name="app.smtp", havingValue="true")
public class MailService {
...
}@ConditionalOnProperty(name="app.smtp", havingValue="true")
例子:假设我们需要保存用户上传的头像,并返回存储路径,在本地开发运行时,我们总是存储到文件:
@Component
@ConditionalOnProperty(name = "app.storage", havingValue = "file", matchIfMissing = true)
public class FileUploader implements Uploader {
...
}
在生产环境运行时,我们会把文件存储到类似AWS S3上:
@Component
@ConditionalOnProperty(name = "app.storage", havingValue = "s3")
public class S3Uploader implements Uploader {
...
}
其他需要存储的服务则注入Uploader:
@Component
public class UserImageService {
@Autowired
Uploader uploader;
}
当应用程序检测到配置文件存在app.storage=s3时,自动使用S3Uploader,如果存在配置app.storage=file,或者配置app.storage不存在,则使用FileUploader。
可见,使用条件注解,能更灵活地装配Bean。
@ConditionalOnClass注解
它可以根据classpath中存在类来判断是否加载bean
如果当前classpath中存在类javax.mail.Transport,则创建MailService:
@Component
@ConditionalOnClass(name = "javax.mail.Transport")
public class MailService {
...
}
3.AOP切面编程
3.1.什么是AOP
AOP是Aspect Oriented Programming,即面向切面编程,回顾下OOP:Object Oriented Programming,OOP作为面向对象编程的模式,获得了巨大的成功,OOP的主要功能是数据封装、继承和多态,而AOP是一种新的编程方式,它和OOP不同,OOP把系统看作多个对象的交互,AOP把系统分解为不同的关注点,或者称之为切面(Aspect)。
AOP主要弥补了OOP的不足,它基于OOP基础之上进行横向开发,OOP规定程序开发以类为主体模型,一切围绕对象进行,完成某个任务先构建模型,AOP程序开发主要关注基于OOP开发中的共性功能,一切围绕共性功能进行,完成某个任务先构建可能遇到的所有共性功能(当所有功能都开发出来也就没有共性与非共性之分)。
举个例子来说,如一个业务组件BookService,它有几个业务方法:
- createBook:添加新的Book;
- updateBook:修改Book;
- deleteBook:删除Book。
对每个业务方法,例如,createBook(),除了业务逻辑,还需要安全检查、日志记录和事务处理,它的代码像这样:
public class BookService {
public void createBook(Book book) {
securityCheck(); //安全检查,每个业务方法中都需要写。
Transaction tx = startTransaction();
try {
// 核心业务逻辑
tx.commit();
} catch (RuntimeException e) {
tx.rollback();
throw e;
}
log("created book: " + book);
}
}
对于安全检查、日志、事务等代码,它们会重复出现在每个业务方法中。使用OOP,我们很难将这些四处分散的代码模块化。
为了解决这个问题我们就可以使用AOP切面编程,将安全检查、日志、事务等每个业务中都会重复出现的功能单独抽离出来(AOP中的切面),然后运行时动态将这些共有功能添加到每个业务中,这样就完美解决了每个业务方法需要重复添加的问题。
但是AOP也是具有局限性的,例如AOP对于解决特定问题,例如事务管理非常有用,这是因为分散在各处的事务代码几乎是完全相同的,并且它们需要的参数(JDBC的Connection)也是固定的。另一些特定问题,如日志,就不那么容易实现,因为日志虽然简单,但打印日志的时候,经常需要捕获局部变量,如果使用AOP实现日志,我们只能输出固定格式的日志,因此,使用AOP时,必须适合特定的场景。
3.2.AOP核心概念
在AOP编程中,我们经常会遇到下面的概念:
- Aspect:切面,即一个横跨多个核心逻辑的功能,或者称之为系统关注点;
- Joinpoint:连接点,即定义在应用程序流程的何处插入切面的执行;
- Pointcut:切入点,即一组连接点的集合;
- Advice:增强,指特定连接点上执行的动作;
- Introduction:引介,指为一个已有的Java对象动态地增加新的接口;
- Weaving:织入,指将切面整合到程序的执行流程中;
- Interceptor:拦截器,是一种实现增强的方式;
- Target Object:目标对象,即真正执行业务的核心逻辑对象;
- AOP Proxy:AOP代理,是客户端持有的增强后的对象引用。
3.3.使用AOP
在maven中引入spring对AOP的依赖
<!--提供了与 AspectJ 框架集成的功能-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<!--AspectJ 的编织器(weaver)
用于在运行时对 Java 代码进行横切(cross-cutting)操作和增强-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
<!--AspectJ 的运行时库(runtime library),
用于在运行时执行被织入的切面(aspects)-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.5</version>
</dependency>
需要被AOP进行增强的类如下
@Component
public class UserService {
@Autowired
private UserMapper userMapper;
//需要被增强的方法
public User login(String name, String password) {
List<User> users=userMapper.getAll();
for (User user : users) {
if (user.getPassword().equalsIgnoreCase(password) && user.getName().equals(name)) {
System.out.println("登录成功!");
return user;
}
}
throw new RuntimeException("登录失败!");
}
}
配置切面和配置切入点通知(下一章节详细讲解切入点通知),其中@Before和@Around是切入点通知,,execution是切入点表达式
@Aspect //配置切面
@Component
public class ServiceAspect {
//配置前置切入点通知,UserService的login方法执行前执行该方法
//execution(访问修饰符 返回值 包名类名.方法名(参数) 异常名)
@Before("execution(public * gc.learn.service.UserService.login(String,String))")
public void doAccessCheck(JoinPoint jp) {
Object[] args = jp.getArgs(); //登录方法中的所有参数
System.out.println(Arrays.toString(args));
System.err.println("登陆前进行参数合法性检查");
}
//配置环绕切入点通知 UserService的login方法执行前后都会执行该方法,
@Around("execution(public * gc.learn.service.UserService.login(String,String))")
public Object doAccessCheck1(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();//登录方法中的所有参数
System.out.println(Arrays.toString(args));
//执行登录方法前先执行
System.err.println("[环绕切入点通知] start " + pjp.getSignature());
//执行login方法,得到login方法执行后的返回值,这里可以用try..catch捕获异常并得到异常信息
Object retVal = pjp.proceed();
//执行登录方法后先执行
System.err.println("[环绕切入点通知] done " + pjp.getSignature());
return retVal;
}
}
在启动配置类中开启AOP支持
@Configuration
@ComponentScan("gc.learn.*")
@EnableAspectJAutoProxy //开启AOP支持
public class App {
public static void main(String[] args) {
annotationConfig();
}
public static void annotationConfig(){
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(App.class);
UserService userService = context.getBean(UserService.class);
User user = userService.login("Bob", "password");
System.out.println(user);
}
}
3.4.切入点和表达式
在使用AOP章节中,我们配置了切入点通知和切入点表达式,其中切入点通知分为很多种类型,切入点表达式也有很多类型,具体如下:
3.4.1.切入点通知类型
@Before("execution(public * gc.learn.service.UserService.login(String,String))")
- @Before前置通知,原始方法执行前执行,如果通知中抛出异常,阻止原始方法运行。
应用:数据校验,通过传入JoinPoint对象参数相关信息 - @After后置通知,原始方法执行后执行,无论原始方法中是否出现异常,都将执行通知
应用:现场清理,通过传入JoinPoint对象参数相关信息 - @AfterReturning返回后通知,原始方法正常执行完毕并返回结果后执行,如果原始方法中抛出异常,无法执行
应用:返回值相关数据处理 - @AfterThrowing抛出异常后通知,原始方法抛出异常后执行,如果原始方法没有抛出异常,无法执行
应用:对原始方法中出现的异常信息进行处理 - @Around环绕通知,在原始方法执行前后均会执行,还可以阻止原始方法的执行。
应用:十分强大,可以做任何事情
3.4.2.切入点表达式类型
@Before("execution(public * gc.learn.service.UserService.login(String,String))")
execution: 用于匹配方法执行的连接点。这是在使用Spring AOP时要使用的主要切点指定器。within: 将匹配限制在某些类型内的连接点(使用Spring AOP时,执行在匹配类型内声明的方法)。this: 将匹配限制在连接点(使用Spring AOP时方法的执行),其中bean引用(Spring AOP代理)是给定类型的实例。target: 将匹配限制在连接点(使用Spring AOP时方法的执行),其中目标对象(被代理的应用程序对象)是给定类型的实例。args: 将匹配限制在连接点(使用Spring AOP时方法的执行),其中参数是给定类型的实例。@target: 限制匹配到连接点(使用Spring AOP时方法的执行),其中执行对象的类有一个给定类型的注解。@args: 将匹配限制在连接点(使用Spring AOP时方法的执行),其中实际传递的参数的运行时类型有给定类型的注解。@within: 将匹配限制在具有给定注解的类型中的连接点(使用Spring AOP时,执行在具有给定注解的类型中声明的方法)。@annotation: 将匹配限制在连接点的主体(Spring AOP中正在运行的方法)具有给定注解的连接点上。
3.4.3.使用@annotation切入点表达式
在实际项目中并不经常使用execution类型的切入点表达式,因为使用它很难发现到底哪个方法被AOP增强了。例如
Around("execution(public * update*(..))")
public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {
// 对update开头的方法切换数据源:
String old = setCurrentDataSource("master");
Object retVal = pjp.proceed();
restoreCurrentDataSource(old);
return retVal;
}
这个方式虽说可以实现update开头的方法名的增强,但是它的覆盖的面太广,如果项目过于复杂庞大,开发者可能不知道设置了该规则的AOP切入点表达式,当在不知配置了该切入点表达式时新增以update开头方法就会莫名被增强(实际上我并不想让这个新增的方法被增强),这时候就会产生不必要的麻烦。
实际上项目开发时更希望像控制事务一样使用一个@Transactional注解明确表示我需要让这个方法受事务控制,例如
@Component
public class UserService {
// 有事务:
@Transactional
public User createUser(String name) {
...
}
// 无事务:
public boolean isValidName(String name) {
...
}
// 有事务:
@Transactional
public void updateUser(User user) {
...
}
}
像上面这样,如果我们将需要增强的方法加上一个特别的注解,只有被添加了这个注解的方法才进行方法增强,这样我们就一目了然的知道哪些方法被AOP增强了,通过使用@annotation切入点表达式可以实现我们想要的效果。
编写自定义注解AopAnnotation,用于方法增强时添加
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AopAnnotation {
String value();
}
需要被AOP进行增强的类如下
@Component
public class UserService {
@Autowired
private UserMapper userMapper;
@AopAnnotation("login") //添加自定义注解,表明该方法需要被aop增强
public User login(String name, String password) {
List<User> users=userMapper.getAll();
for (User user : users) {
if (user.getPassword().equalsIgnoreCase(password) && user.getName().equals(name)) {
System.out.println("登录成功!");
return user;
}
}
throw new RuntimeException("登录失败!");
}
}
配置切面类,并配置@annotation表达式类型匹配注解,如果包含aopAnnotation注解就执行增强方法,注意,aopAnnotation其实是参数中的参数名,起实际作用的是参数类型为自定义的注解类型。
@Aspect //配置切面
@Component
public class ServiceAspect {
@After("@annotation(aopAnnotation)") //使用@annotation表达式类型匹配注解
public void doAccessCheck3(JoinPoint jp, AopAnnotation aopAnnotation) {
String value = aopAnnotation.value();
System.out.println("注解中的参数:"+value);
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.err.println("@annotation : login方法执行后执行");
}
}
4.数据库访问
4.1.通过JdbcTemplate访问
我这边通过JdbcTemplate访问mysql数据库为例子
添加依赖
<!--jdbc依赖包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<!--mysql连接驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>
创建jdbc连接池和JdbcTemplate实例
@Configuration
@PropertySource("value = "classpath:application.properties",encoding = "utf-8"")
public class DatabaseConfig {
@Value("${database.url}")
private String jdbcUrl;
@Value("${database.username}")
private String userName;
@Value("${database.password}")
private String password;
@Bean //创建连接池
public DataSource createDataSource(){
MysqlDataSource dataSource = new MysqlDataSource();
dataSource.setURL(this.jdbcUrl);
dataSource.setUser(this.userName);
dataSource.setPassword(this.password);
return dataSource;
}
@Bean //创建JdbcTemplate实例
JdbcTemplate createJdbcTemplate(@Autowired DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
application.properties文件内容如下
database.url=jdbc:mysql://localhost:3306/spring-database?characterEncoding=UTF-8
database.username=root
database.password=111111
JdbcTemplate访问mysql数据库,增删查改
@Component
public class UserMapper {
@Autowired
JdbcTemplate jdbcTemplate;
public void testMysql() {
//执行任何sql,增加删除查询更新的等
List<User> userList = jdbcTemplate.execute("select * from user LIMIT ?,?", (PreparedStatement var1) -> {
var1.setObject(1, 0);
var1.setObject(2, 1);
ResultSet resultSet = var1.executeQuery();
List<User> users = new ArrayList<>();
while (resultSet.next()) {
long id = resultSet.getLong("id");
String name = resultSet.getString("name");
String password = resultSet.getString("password");
User user = new User(id + "", name, password);
users.add(user);
}
return users;
});
System.out.println(userList);
//查询 返回单条结果
try {
User user = jdbcTemplate.queryForObject("select * from user where name=? and password=?", (ResultSet var1, int var2) -> {
if (!var1.wasNull()) {
String id = var1.getString("id");
String name = var1.getString("name");
String password = var1.getString("password");
return new User(id, name, password);
} else {
return null;
}
}, "nob", "1");
System.out.println(user);
//查询 返回多条结果
} catch (Exception e) {
System.out.println("用户未找到");
}
//查询 返回多条语句
List<User> userList1 = jdbcTemplate.query("select * from user LIMIT ?,?",
new BeanPropertyRowMapper<>(User.class), 0, 10);
System.out.println(userList1);
//更新删除添加
if (1 != jdbcTemplate.update("UPDATE user SET name = ? WHERE id = ?", "Bob1", "1")) {
throw new RuntimeException("User not found by id");
}
//更新删除添加 返回主键,多用于数据库主键自增
// 创建一个KeyHolder:
KeyHolder holder = new GeneratedKeyHolder();
if (1 != jdbcTemplate.update(
// 参数1:PreparedStatementCreator
(conn) -> {
// 创建PreparedStatement时,必须指定RETURN_GENERATED_KEYS:
PreparedStatement ps = conn.prepareStatement("INSERT INTO user(name, password) VALUES(?, ?)",
Statement.RETURN_GENERATED_KEYS);
ps.setObject(1, "aaaaa");
ps.setObject(2, "111111");
return ps;
},
// 参数2:KeyHolder
holder)
) {
throw new RuntimeException("Insert failed.");
}
System.out.println("自增主键:"+holder.getKey().toString());
}
}
4.2.数据库事务
4.2.1.什么是数据库事务
事务(Transaction)指一个操作,由多个步骤组成,要么全部成功,要么全部失败。
比如我们常用的转账功能,假设A账户向B账号转账,那么涉及两个操作:
从 A 账户扣钱。
往 B 账户加入等量的钱。
因为是独立的两个操作,所以可能有一个成功,一个失败的情况。但是因为在这种场景下,不能存在从 A 账户扣钱成功,往 B 账户加入等量钱失败这种情况,要么同时成功,要么同时失败(一个失败需要回滚),即必须要保证事务。
4.2.2.使用事务
在Spring中操作事务,没必要手写JDBC事务,可以使用Spring提供的高级接口来操作事务。Spring提供了一个PlatformTransactionManager来表示事务管理器,所有的事务都由它负责管理。
通过@EnableTransactionManagement来开启数据库事务支持。
@Configuration
@PropertySource(value = "classpath:application.properties",encoding = "utf-8")
@EnableTransactionManagement //开启数据库事务支持
public class DatabaseConfig {
@Value("${database.url}")
private String jdbcUrl;
@Value("${database.username}")
private String userName;
@Value("${database.password}")
private String password;
@Bean
public DataSource createDataSource(){
MysqlDataSource dataSource = new MysqlDataSource();
dataSource.setURL(this.jdbcUrl);
dataSource.setUser(this.userName);
dataSource.setPassword(this.password);
return dataSource;
}
@Bean
JdbcTemplate createJdbcTemplate(@Autowired DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean //创建事务实例
PlatformTransactionManager createTxManager(@Autowired DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
通过添加@Transactional进行事务控制,当方法出现运行异常时,事务将会回滚。@Transactional本质上就是通过AOP切面实现的
@Component
public class UserMapper {
@Autowired
JdbcTemplate jdbcTemplate;
@Transactional //对该方法进行事务控制
public void insert(){
jdbcTemplate.execute("INSERT INTO user(id,name, password) VALUES(?,?, ?)",(PreparedStatement var1)->{
var1.setObject(1,"11555");
var1.setObject(2,"zzzzz");
var1.setObject(3,"555555");
int i = var1.executeUpdate();
if (i>=1){
System.out.println("添加成功!");
}else {
System.out.println("添加失败!");
}
return i;
});
int s=1/0; //如果发生运行异常,事务将进行回滚,两条插入语句都将将失败
jdbcTemplate.execute("INSERT INTO user(id,name, password) VALUES(?,?, ?)",(PreparedStatement var1)->{
var1.setObject(1,"18888");
var1.setObject(2,"hhhh");
var1.setObject(3,"555555");
int i = var1.executeUpdate();
if (i>=1){
System.out.println("添加成功!");
}else {
System.out.println("添加失败!");
}
return i;
});
}
}
4.2.3.事务边界与事务传播
事务边界
在使用事务的时候,明确事务边界非常重要。对于声明式事务,例如,下面的register()方法:
@Component
public class UserService {
@Transactional
public User register(String email, String password, String name) { // 事务开始
...
} // 事务结束
}
它的事务边界就是register()方法开始和结束。
在现实世界中,问题总是要复杂一点点。用户注册后,能自动获得100积分,因此,实际代码如下:
@Component
public class UserService {
@Autowired
BonusService bonusService;
@Transactional
public User register(String email, String password, String name) {
// 插入用户记录:该方法是否被事务管控了?
User user = jdbcTemplate.insert("...");
// 增加100积分:该方法是否被事务管控了?
bonusService.addBonus(user.id, 100);
}
}
现在问题来了:调用方(比如RegisterController)调用UserService.register()这个事务方法,它在内部又调用了BonusService.addBonus()这个事务方法,一共有几个事务?如果addBonus()抛出了异常需要回滚事务,register()方法的事务是否也要回滚?
事务传播
我们需要关心的问题是,在UserService.register()这个事务方法内,调用BonusService.addBonus(),我们期待的事务行为是什么:
@Transactional
public User register(String email, String password, String name) {
// 事务已开启:
User user = jdbcTemplate.insert("...");
// ???:
bonusService.addBonus(user.id, 100);
} // 事务结束
对于大多数业务来说,我们期待BonusService.addBonus()的调用,和UserService.register()应当融合在一起,它的行为应该如下:
UserService.register()已经开启了一个事务,那么在内部调用BonusService.addBonus()时,BonusService.addBonus()方法就没必要再开启一个新事务,直接加入到BonusService.register()的事务里就好了。
其实就相当于:
UserService.register()先执行了一条INSERT语句:INSERT INTO users ...BonusService.addBonus()再执行一条INSERT语句:INSERT INTO bonus ...
因此,Spring的声明式事务为事务传播定义了几个级别,默认传播级别就是REQUIRED,它的意思是,如果当前没有事务,就创建一个新事务,如果当前有事务,就加入到当前事务中执行。
默认的事务传播级别是REQUIRED,它满足绝大部分的需求。还有一些其他的传播级别:
SUPPORTS:表示如果有事务,就加入到当前事务,如果没有,那也不开启事务执行。这种传播级别可用于查询方法,因为SELECT语句既可以在事务内执行,也可以不需要事务;
MANDATORY:表示必须要存在当前事务并加入执行,否则将抛出异常。这种传播级别可用于核心更新逻辑,比如用户余额变更,它总是被其他事务方法调用,不能直接由非事务方法调用;
REQUIRES_NEW:表示不管当前有没有事务,都必须开启一个新的事务执行。如果当前已经有事务,那么当前事务会挂起,等新事务完成后,再恢复执行;
NOT_SUPPORTED:表示不支持事务,如果当前有事务,那么当前事务会挂起,等这个方法执行完成后,再恢复执行;
NEVER:和NOT_SUPPORTED相比,它不但不支持事务,而且在监测到当前有事务时,会抛出异常拒绝执行;
NESTED:表示如果当前有事务,则开启一个嵌套级别事务,如果当前没有事务,则开启一个新事务。
那一个事务方法,如何获知当前是否存在事务?
答案是使用ThreadLocal。Spring总是把JDBC相关的Connection和TransactionStatus实例绑定到ThreadLocal。如果一个事务方法从ThreadLocal未取到事务,那么它会打开一个新的JDBC连接,同时开启一个新的事务,否则,它就直接使用从ThreadLocal获取的JDBC连接以及TransactionStatus。
因此,事务能正确传播的前提是,方法调用是在一个线程内才行。如果像下面这样写:
@Transactional
public User register(String email, String password, String name) { // BEGIN TX-A
User user = jdbcTemplate.insert("...");
new Thread(() -> {
// BEGIN TX-B:
bonusService.addBonus(user.id, 100);
// END TX-B
}).start();
} // END TX-A
在另一个线程中调用BonusService.addBonus(),它根本获取不到当前事务,因此,UserService.register()和BonusService.addBonus()两个方法,将分别开启两个完全独立的事务。
换句话说,事务只能在当前线程传播,无法跨线程传播。
4.3.DAO数据持久层(mapper数据持久层)
DAO即Data Access Object的缩写,其实就是对数据库访问方法的进一步抽象封装。
数据持久层usermapper
@Component
public class UserMapper extends AbstractDao<User>{
}
数据持久层进一步抽象
public class AbstractDao<T> extends JdbcDaoSupport {
@Autowired
JdbcTemplate jdbcTemplate;
private String table;
private Class<T> entityClass;
private RowMapper<T> rowMapper;
public AbstractDao(){
// 获取当前泛型类型T的具体类:
this.entityClass = getParameterizedType();
//根据具体类,将该类名作为数据库表的表名
this.table = this.entityClass.getSimpleName().toLowerCase();
//把ResultSet的一行记录映射为User对象
this.rowMapper = new BeanPropertyRowMapper<>(entityClass);
}
@PostConstruct //自定义的类初始化执行回调
public void init() {
super.setJdbcTemplate(jdbcTemplate);
}
public T getById(long id) {
return getJdbcTemplate().queryForObject("SELECT * FROM " + table + " WHERE id = ?", this.rowMapper, id);
}
public List<T> getAll(int pageIndex) {
int limit = 100;
int offset = limit * (pageIndex - 1);
return getJdbcTemplate().query("SELECT * FROM " + table + " LIMIT ? OFFSET ?",
new Object[] { limit, offset },
this.rowMapper);
}
public void deleteById(long id) {
getJdbcTemplate().update("DELETE FROM " + table + " WHERE id = ?", id);
}
@SuppressWarnings("unchecked")
private Class<T> getParameterizedType() {
//得到的是UserMapper类型字节码
Class<?> aClass = getClass();
//得到AbstractDao类型
Type type = getClass().getGenericSuperclass();
if (!(type instanceof ParameterizedType)) {
throw new IllegalArgumentException("Class " + getClass().getName() + " does not have parameterized type.");
}
//强转为ParameterizedType,还是AbstractDao类型
ParameterizedType pt = (ParameterizedType) type;
//获取所有的泛型T的实际类型
Type[] types = pt.getActualTypeArguments();
if (types.length != 1) {
throw new IllegalArgumentException("Class " + getClass().getName() + " has more than 1 parameterized types.");
}
//得到实际的User类型
Type r = types[0];
if (!(r instanceof Class<?>)) {
throw new IllegalArgumentException("Class " + getClass().getName() + " does not have parameterized type of class.");
}
//转为User类型字节码
return (Class<T>) r;
}
}
4.4.通过Hibernate访问数据库
4.4.1.什么是Hibernate
使用JdbcTemplate的时候,我们用得最多的方法就是List<T> query(String, RowMapper, Object...)。这个RowMapper的作用就是把ResultSet的一行记录映射为Java Bean。
这种把关系数据库的表记录映射为Java对象的过程就是ORM:Object-Relational Mapping。ORM既可以把记录转换成Java对象,也可以把Java对象转换为行记录。
使用JdbcTemplate配合RowMapper可以看作是最原始的ORM。如果要实现更自动化的ORM,可以选择成熟的ORM框架,例如:Hibernate
4.4.2.使用Hibemate框架
引入Hibemate和orm依赖
<!--mysql连接驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>
<!-- ORM框架 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.2.12.RELEASE</version>
</dependency>
<!-- hibernate框架 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.2.12.Final</version>
</dependency>
创建mysql连接池和Hibernate实例
@Configuration
@PropertySource(value = "classpath:application.properties",encoding = "utf-8")
@EnableTransactionManagement
public class DatabaseConfig {
@Value("${database.url}")
private String jdbcUrl;
@Value("${database.username}")
private String userName;
@Value("${database.password}")
private String password;
@Bean
public DataSource createDataSource(){
MysqlDataSource dataSource = new MysqlDataSource();
dataSource.setURL(this.jdbcUrl);
dataSource.setUser(this.userName);
dataSource.setPassword(this.password);
return dataSource;
}
@Bean
public LocalSessionFactoryBean createSessionFactory(@Autowired DataSource dataSource) {
Properties props = new Properties();
//表示自动创建数据库的表结构,注意不要在生产环境中启用
props.setProperty("hibernate.hbm2ddl.auto", "update");
//指示Hibernate使用的数据库是HSQLDB。
//Hibernate使用一种HQL的查询语句,它和SQL类似,但真正在“翻译”成SQL时,会根据设定的数据库“方言”来生成针对数据库优化的SQL,不同数据库设置不同我这里用的msql5.x所以对应的hibernate.dialect的值为org.hibernate.dialect.MySQLInnoDBDialect
props.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQLInnoDBDialect");
//让Hibernate打印执行的SQL,这对于调试非常有用,我们可以方便地看到Hibernate生成的SQL语句是否符合我们的预期。
props.setProperty("hibernate.show_sql", "true");
LocalSessionFactoryBean sessionFactoryBean = new LocalSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);
// 扫描指定的package获取所有entity class:
sessionFactoryBean.setPackagesToScan("gc.learn.entity");
sessionFactoryBean.setHibernateProperties(props);
return sessionFactoryBean;
}
@Bean //创建Hibernate事务管理
public PlatformTransactionManager createHibernateTxManager(@Autowired SessionFactory sessionFactory) {
return new HibernateTransactionManager(sessionFactory);
}
}
LocalSessionFactoryBean是一个FactoryBean,它会再自动创建一个SessionFactory,在Hibernate中,Session是封装了一个JDBC Connection的实例,而SessionFactory是封装了JDBC DataSource的实例,即SessionFactory持有连接池,每次需要操作数据库的时候,SessionFactory创建一个新的Session,相当于从连接池获取到一个新的Connection。SessionFactory就是Hibernate提供的最核心的一个对象,但LocalSessionFactoryBean是Spring提供的为了让我们方便创建SessionFactory的类。
常用的设置请参考Hibernate文档
编写实体类User
该类用于将实体类和数据库字段进行映射
@Entity
@Table(name = "user") //数据库中的表名
public class User {
private String id;
private String name;
private String password;
public User(){};
public User(String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
public User(String id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false)
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@Column(nullable = false, unique = true, length = 100)
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Column(nullable = false, unique = true, length = 100)
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Transient
public ZonedDateTime getCreatedDateTime() {
return Instant.ofEpochMilli(this.createdAt).atZone(ZoneId.systemDefault());
}
@Override
public String toString() {
return "User{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", password='" + password + '\'' +
'}';
}
}
每个属性到数据库列的映射用@Column()标识,nullable指示列是否允许为NULL,updatable指示该列是否允许被用在UPDATE语句,length指示String类型的列的长度(如果没有指定,默认是255)。
@Transient注解表示,它返回一个“虚拟”的属性。因为getCreatedDateTime()是计算得出的属性,而不是从数据库表读出的值,因此必须要标注@Transient,否则Hibernate会尝试从数据库读取名为createdDateTime这个不存在的字段从而出错。
@MappedSuperclass表示它用于被继承的类
对于主键,还需要用@Id标识,自增主键再追加一个@GeneratedValue,以便Hibernate能读取到自增主键的值。
注意:实体类中需要使用包装类型,不能是基本数据类型。
插入、删除、修改操作
@Component
public class UserMapperByHibernate {
@Autowired
SessionFactory sessionFactory;
@Transactional //添加删除修改操作Hibernate必须开启事务支持
public void testHibernate() {
//插入操作
User user = new User("454","45454");
sessionFactory.getCurrentSession().persist(user);//调用persist()来添加
//获得自增id:
System.out.println(user.getId());
System.out.println(user);
//删除操作
User user1 = sessionFactory.getCurrentSession().byId(User.class).load("5555");
if (user1 != null) {
sessionFactory.getCurrentSession().remove(user);//调用remove()来删除
System.out.println("删除成功");
}else{
System.out.println("删除失败");
}
//修改操作
User user2 = sessionFactory.getCurrentSession().byId(User.class).load("2");
user.setName("111111");
sessionFactory.getCurrentSession().merge(user);//merge()来修改
}
}
使用原生sql进行查询
@Component
public class UserMapperByHibernate {
@Autowired
SessionFactory sessionFactory;
@Transactional
public void testHibernateQuery(){
//使用原始sql查询多条数据
List<User> list = sessionFactory.getCurrentSession()
.createNativeQuery("select * from user limit ?1,?2", User.class)
.setParameter(1, 0).setParameter(2, 5)
.list();
System.out.println(list);
}
}
4.5.使.用mybatis框架访问数据库
4.5.1.什么是mybatis框架
MyBatis是一种半自动化ORM框架,介于全自动ORM如Hibernate和手写全部如JdbcTemplate之间,它只负责把ResultSet自动映射到Java Bean,或者自动填充Java Bean参数,但仍需自己写出SQL。
4.5.2.使用mybatis框架
添加mybatis依赖
<!--mysql连接驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>
<!--spring整合mybatis-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.0</version>
</dependency>
<!--mybatis核心依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.11</version>
</dependency>
创建连接池和myabtis实例
@Configuration
@PropertySource(value = "classpath:application.properties",encoding = "utf-8"")
@EnableTransactionManagement
@MapperScan("gc.learn.mapper") //指定扫描的mapper接口
public class DatabaseConfig {
@Value("${database.url}")
private String jdbcUrl;
@Value("${database.username}")
private String userName;
@Value("${database.password}")
private String password;
@Bean
public DataSource createDataSource(){
MysqlDataSource dataSource = new MysqlDataSource();
dataSource.setURL(this.jdbcUrl);
dataSource.setUser(this.userName);
dataSource.setPassword(this.password);
return dataSource;
}
@Bean //创建jdbc事务实例,mybatis使用的是jdbc事务实例
public PlatformTransactionManager createTxManager(@Autowired DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean //创建mybatis实例
SqlSessionFactoryBean createSqlSessionFactoryBean(@Autowired DataSource dataSource) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
//XML映射文件位置,不配置默认路径需要和mapper接口一致
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
Resource[] resource = resourceResolver.getResources("classpath:mapper/*Mapper*.xml");
sqlSessionFactoryBean.setMapperLocations(resource);
//配置控制台打印sql
org.apache.ibatis.session.Configuration configuration =new org.apache.ibatis.session.Configuration();
configuration.setLogImpl(org.apache.ibatis.logging.stdout.StdOutImpl.class);
sqlSessionFactoryBean.setConfiguration(configuration);
return sqlSessionFactoryBean;
}
}
编写mapper接口
@Mapper
public interface UserMapperByMybatis {
//增加
@Insert("INSERT INTO user (name, password) VALUES (#{user.name}, #{user.password})")
Integer insert(@Param("user") User user);
//删除
@Delete("DELETE FROM user WHERE id = #{id}")
void deleteById(@Param("id") long id);
//修改
@Update("UPDATE user SET name = #{user.name} WHERE id = #{user.id}")
void update(@Param("user") User user);
//查询,返回多条记录
@Select("SELECT * FROM user LIMIT #{offset}, #{maxResults}")
List<User> getAll(@Param("offset") int offset, @Param("maxResults") int maxResults);
}
以上是注解方式配置,mybatis运行使用xml方式进行配置,好处是可以可以动态组装SQL,例如in查询可以在xml中用循环解决。
resources/mapper/userMapperByMybatis.xml文件内容如下,因为我配置文件中配置了映射文件位置为classpath:mapper/*Mapper.xml,如果不配置路径则需要userMapperByMybatis.xml和userMapperByMybatis接口路径保持一致*
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="gc.learn.mapper.UserMapperByMybatis">
<resultMap id="userResultMap" type="gc.learn.entity.User">
<id property="id" column="id" />
<result property="name" column="name"/>
<result property="password" column="password"/>
</resultMap>
<select id="getById" resultMap="userResultMap">
select * from user where id = #{id}
</select>
<update id="updateUser">
UPDATE user SET
<set>
<if test="user.name != null"> name = #{user.name} </if>
<if test="user.password != null"> hobby = #{user.hobby} </if>
</set>
WHERE id = #{user.id}
</update>
<select id="selectPostIn" resultMap="userResultMap">
SELECT *
FROM user
WHERE ID in
<foreach item="item" index="index" collection="list"
open="(" separator="," close=")">
#{item}
</foreach>
</select>
</mapper>
使用MyBatis最大的问题是所有SQL都需要全部手写,优点是执行的SQL就是我们自己写的SQL,对SQL进行优化非常简单,也可以编写任意复杂的SQL,或者使用数据库的特定语法,但切换数据库可能就不太容易。好消息是大部分项目并没有切换数据库的需求,完全可以针对某个数据库编写尽可能优化的SQL。
4.6.手写ORM框架
这里我们准备实现的ORM并不想要全自动ORM那种自动读取一对多和多对一关系的功能,也不想给Entity加上复杂的状态,因此,对于Entity来说,它就是纯粹的JavaBean,没有任何Proxy。
此外,ORM要兼顾易用性和适用性。易用性是指能覆盖95%的应用场景,但总有一些复杂的SQL,很难用ORM去自动生成,因此,也要给出原生的JDBC接口,能支持5%的特殊需求。
最后,我们希望设计的接口要易于编写,并使用流式API便于阅读。为了配合编译器检查,还应该支持泛型,避免强制转型。
TODO
5.Web应用(spring mvc)
5.1.spring整合WebMVC
5.1.1.什么是WebMVC
之前我们在servlet那篇文章中讲解了MVC框架和servlet相关知识,总的来说servlet有以下几个重要组件
- Servlet:能处理HTTP请求并将HTTP响应返回;
- JSP:一种嵌套Java代码的HTML,将被编译为Servlet;
- Filter:能过滤指定的URL以实现拦截功能;
- Listener:监听指定的事件,如ServletContext、HttpSession的创建和销毁。
其实WebMVC本质就是Servlet+视图解析模板+Contrller控制层,在Servlet那篇文章中讲解了MVC的实现,现在我们可以不用手写了,通过引入WebMVC依赖就可以完成一个web应用实现下面这种效果。
@Controller
public class UserController {
@GetMapping("/register")
public ModelAndView register() {
...
}
@PostMapping("/signin")
public ModelAndView signin(@RequestParam("email") String email, @RequestParam("password") String password) {
...
}
...
}
5.1.2.使用WebMVC
引入WebMVC依赖
<!--servlet依赖-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!--引入webmvc依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.12.RELEASE</version>
</dependency>
<!--thymeleaf模板引擎-->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.15.RELEASE</version>
</dependency>
webMcv配置类,用来配置包扫描,扫描注解。
@Configuration
@ComponentScan("gc.learn.*")
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
/*
* 开启对静态资源的访问
* 类似在Spring MVC的xml配置文件中设置<mvc:default-servlet-handler/>元素
*/
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
//放行静态资源
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//将网络请求路径/static/** 映射到本地文件路径/WEB-INF/static/,
//本地路径我测试是不能添加classpath否则加载不到,而且必须有要有/WEB-INF/
registry.addResourceHandler("/static/**")
.addResourceLocations("/WEB-INF/static/");
}
}
webmvc容器启动配置类,加载Spring的ioc容器,以前我们是通过主方法来启动Ioc容器的,现在因为我们需要在web服务器中运行,所以要启动ioc容器就需要通过Web服务器来帮我们启动,当继承AbstractDispatcherServletInitializer类并实现以下三个方法时,Web服务器启动时就会调用我们实现的三个方法,通过这三个方法就能完成spring IOC容器的加载。
在SpringMVC中,有其实有两个应用上下文:RootApplicationContext、WebApplicationContext。其中,WebApplicationContext是DispatcherServlet专属的上下文,用来加载Controller、ViewResolver、HandlerMapping等web相关的Bean。RootApplicationContext则是加载数据库、@Service等中间件中的Bean。其中,WebApplicationContext继承了RootApplicationContext中的所有Bean,以便在@Controller中注入@Service等依赖。
//继承AbstractDispatcherServletInitializer类,
//当Web服务器启动时会调用继承过来的createServletApplicationContext方法
public class ServletConfig extends AbstractDispatcherServletInitializer {
//当Web服务器启动时该方法会最先调用,主要用来加载spring的ioc容器
@Override
protected WebApplicationContext createServletApplicationContext() {
//AnnotationConfigWebApplicationContext相当于Spring的Ioc容器,
AnnotationConfigWebApplicationContext webApplicationContext
= new AnnotationConfigWebApplicationContext();
//通过register方法加载配置类
webApplicationContext.register(WebMvcConfig.class);
return webApplicationContext;
}
//当前web服务其启动完成后会调用该方法,该方法可以用来配置filter过滤器和Listener监听器
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
//创建一个字符编码过滤器,处理所有请求的字符编码
CharacterEncodingFilter characterEncodingFilter
= new CharacterEncodingFilter();
characterEncodingFilter.setEncoding("utf-8");
templateResolver.setCacheable(false);
templateResolver.setTemplateMode("HTML5");
//将过滤器添加到servlet运行容器中
FilterRegistration.Dynamic filterRegistration
= servletContext.addFilter("characterEncodingFilter", characterEncodingFilter);
//设置对哪些请求进行拦截过滤
filterRegistration.addMappingForServletNames(
EnumSet.of(DispatcherType.REQUEST,DispatcherType.INCLUDE,DispatcherType.FORWARD),
false,"/*");
}
//配置servlet拦截所有请求
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
}
视图引擎配置
我这里用的模板是thymeleaf模板
@Configuration
public class ViewEngineConfig {
//模板解析器,我这里配置的是thymeleaf模板解析器
@Bean
public SpringResourceTemplateResolver createTemplateResolver(){
SpringResourceTemplateResolver templateResolver
= new SpringResourceTemplateResolver();
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
templateResolver.setCharacterEncoding("utf-8");
templateResolver.setCacheable(false);
templateResolver.setTemplateMode("HTML5");
return templateResolver;
}
//模板引擎,我这里配置的是thymeleaf模板引起
@Bean
public SpringTemplateEngine springTemplateEngine(SpringResourceTemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
//视图解析器,我这里配置的是thymeleaf试图解析器
@Bean
public ThymeleafViewResolver viewResolver(SpringTemplateEngine springTemplateEngine) {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(springTemplateEngine);
viewResolver.setCharacterEncoding("utf-8");
return viewResolver;
}
}
让后在main路径下创建webapp目录,创建WEB-INF目录等,对应的目录结构如下,然后就是将maven项目改造为maven web应用,可以参考servlet学习笔记中的JavaServlet项目创建。

base.html内容如下
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org" th:fragment="html (head,content)">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>MVC</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<!-- <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.1.1/css/bootstrap.css" rel="stylesheet">-->
<link href="/static/css/bootstrap.css" rel="stylesheet">
</head>
<body style="font-size:16px" >
<div class="d-flex flex-column flex-md-row align-items-center p-3 px-md-4 mb-3 bg-white border-bottom shadow-sm">
<h5 class="my-0 mr-md-auto font-weight-normal"><a href="/">Home</a></h5>
<nav class="my-2 mr-md-3">
<a class="p-2 text-dark" target="_blank" href="https://www.leduo.work/">Learn</a>
<a class="p-2 text-dark" target="_blank" href="#">Source</a>
</nav>
<th:block th:if="${user==null}">
<a href="/signin" class="btn btn-outline-primary">Sign In</a>
</th:block>
<th:block th:if="${user!=null}">
<span>Welcome, <a href="/user/profile">[[${user.name}]]</a></span>
<a href="/signout" class="btn btn-outline-primary">Sign Out</a>
</th:block>
</div>
<div class="container" style="max-width: 960px">
<div class="row">
<div class="col-12 col-md">
<section>
<th:block th:replace="${content}"/>
</section>
</div>
</div>
<footer class="pt-4 my-md-5 pt-md-5 border-top">
<div class="row">
<div class="col-12 col-md">
<h5>Copyright©2020</h5>
<p>
<a target="_blank" href="https://www.leduo.work/">Learn</a> -
<a target="_blank" href="https://www.leduo.work/">Download</a> -
<a target="_blank" href="https://www.leduo.work/">Github</a>
</p>
</div>
</div>
</footer>
</div>
</body>
</html>
index.html内容如下
<div th:replace="~{base :: html(head = null,content = ~{::content})}">
<th:block th:fragment="content">
<!-- <h3>Welcome [[${user.name}]]!</h3>-->
<th:block th:if="${user!=null}">
<h3>Welcome [[${user.name}]]!</h3>
</th:block>
<th:block th:if="${user==null}">
<h3>Welcome</h3>
</th:block>
</th:block>
</div>
编写controller控制层代码
@Controller
public class IndexController {
@Autowired
UserService userService;
@GetMapping("/")
public ModelAndView index(){
userService.getUserById();
User user = new User("AL", "666666");
ModelAndView index = new ModelAndView();
index.setViewName("index");
index.addObject("user",user);
return index;
}
}
运行后结果如下:

5.2.请求和响应
5.2.1.请求request
@RequestMapping请求注解
这请求注解是个万能注解,它可以接受任何类型的请求,例如get、post、put、delete、Patch等。get请求是没有请求体的,而post是有请求体的,直接的不同就是网络请求中的Content-Type属性,我们熟悉的form表单post请求的Content-Type属性值为“application/x-www-form-urlencoded; charset=UTF-8”,而json数据对应的Content-Type属性值为“application/json;charset=UTF-8”,所以当需要发起json数据的post请求时,需要设置Content-Type属性值为“application/json;charset=UTF-8”。
用作用于类或者方法上、主要参数属性有value和method,其中value为网络请求路径,method为网络请求类型
- 例如要查询用户,就使用
GET请求(没有请求体) - 例如要新增用户,就使用
POST请求(有请求体) - 例如要更新用户,就使用
PUT请求(可以有请求体,必须为json) - 例如要删除用户,就使用
DELETE请求(可以有请求体,必须为json)
映射基本数据类型
网络请求参数名称和方法参数名称匹配,如果要获取原始请求对象可以在参数中添加HttpServletRequest对象参数,通过它可以拿到session对象、Cookies对象等,这个就是servlet类中的原始请求对象。不管是什么类型的网络请求,只要参数匹配就能完成映射。
//http://127.0.0.1:8080/rm?name=小明&password=123456,参数名和方法名一一对应
@RequestMapping(value = "/rm",method = RequestMethod.GET)
public ModelAndView requestMapping(String name,String password){
User user = new User(name, password);
HashMap<String, User> hashMap = new HashMap<>();
hashMap.put("user",user);
return new ModelAndView("index",hashMap);
}
请求参数名称和方法参数名称不匹配时,通过@RequestParam来进行矫正匹配,它需要写在方法参数中,通过将请求username矫正匹配为name
//http://127.0.0.1:8080/rm1?username=小明&password=123456
@RequestMapping(value = "/rm1",method = RequestMethod.GET)
public ModelAndView requestMapping1(@RequestParam(name = "username") String name,
String password){
User user = new User(name, password);
HashMap<String, User> hashMap = new HashMap<>();
hashMap.put("user",user);
return new ModelAndView("index",hashMap);
}
restful风格,通过路径匹配参数,需要通过注解**@PathVariable**进行参数修饰,用来匹配请求路径中的参数,delete请求,只能通过路径参数匹配获取值,put同样也只能也只能通过路径匹配获取值,如果要映射成为实体类就只能通过json映射。
//http://127.0.0.1:8080/rm1/小明/123456,通过路径匹配,@PathVariable("name") 可以通过参数纠正匹配参数
//@RequestMapping(value = "/rm1/{name}/{pssword}",method = RequestMethod.DELETE)
//@RequestMapping(value = "/rm1/{name}/{pssword}",method = RequestMethod.PUT)
@RequestMapping(value = "/rm1/{name}/{pssword}",method = RequestMethod.GET)
public ModelAndView requestMapping1_1(@PathVariable String name, @PathVariable String password){
User user = new User(name, password);
HashMap<String, User> hashMap = new HashMap<>();
hashMap.put("user",user);
return new ModelAndView("index",hashMap);
}
因为前端的form表单默认不支持restful风格的,所以需要需要配置一个filter(HiddenHttpMethodFilter对象)来让form表单支持restfull风格。
//继承AbstractDispatcherServletInitializer类,
//当Web服务器启动时会调用继承过来的createServletApplicationContext方法
public class ServletConfig extends AbstractDispatcherServletInitializer {
//当Web服务器启动时该方法会最先调用,主要用来加载spring的ioc容器
@Override
protected WebApplicationContext createServletApplicationContext() {
//AnnotationConfigWebApplicationContext相当于Spring的Ioc容器,
AnnotationConfigWebApplicationContext webApplicationContext
= new AnnotationConfigWebApplicationContext();
//通过register方法加载配置类
webApplicationContext.register(WebMvcConfig.class);
return webApplicationContext;
}
//当前web服务其启动完成后会调用该方法,该方法可以用来配置filter过滤器和Listener监听器
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
//添加restful风格支持
HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
FilterRegistration.Dynamic hiddenHttpMethodFilter1 = servletContext.addFilter("hiddenHttpMethodFilter", hiddenHttpMethodFilter);
hiddenHttpMethodFilter1.addMappingForServletNames( EnumSet.of(DispatcherType.REQUEST,DispatcherType.INCLUDE,DispatcherType.FORWARD),
false,"/*");
}
//配置servlet拦截所有请求
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
}
然后form表单就需要写成下面这种形式进行提交,注意:name的值必须为“_method”。因为现在基本都是使用Ajax等技术来发起请求了,所以直接使用form表单来发起restful风格请求很少,下面就是一个form表单发起restful风格请求。

映射复杂参数类型
复杂类型就是对象类型,例如:实体类、集合、数组等
映射为实体类(键值数据对映射为实体类),需要网络请求参数和实体类属性一一对应,未对应的参数将无法完成映射也就是参数值为null。
//http://127.0.0.1:8080/rm1?name=小明&password=123456
@RequestMapping(value = "rm2",method = RequestMethod.GET)
public ModelAndView requestMapping2(User user){
HashMap<String, User> hashMap = new HashMap<>();
hashMap.put("user",user);
return new ModelAndView("index",hashMap);
}
映射为实体类(json数据映射为实体类),json数据必须要通过post请求发起,所以网络请求类型必须为post,而且需要设置Content-Type属性值为“application/json;charset=UTF-8”,同时需要使用json转换工具将json转换为对应实体,可以用市面上的jackson工具或者fastjson来完成转换,这里使用jackson工具(它不需要手动配置,而fastjson需要手动配置)。
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.0-rc2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.13.0-rc2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.13.0-rc2</version>
</dependency>
@RequestBody表名该参数是json请求体内容,需要将其转换为实体类。
//接收json数据,并通过jackson转换为User实体类
@RequestMapping(value = "rm01",method = RequestMethod.POST)
public ModelAndView requestMapping01(@RequestBody User user) throws IOException {
HashMap<String, User> hashMap = new HashMap<>();
hashMap.put("user",user);
return new ModelAndView("index",hashMap);
}
restful风格映射为实体类(json数据映射为实体类)
@RequestMapping(value = "rm02",method = RequestMethod.PUT)
public ModelAndView requestMapping02(@RequestBody User user) throws IOException {
HashMap<String, User> hashMap = new HashMap<>();
hashMap.put("user",user);
return new ModelAndView("index",hashMap);
}
restful风格映射为集合(json数据映射为集合)
@PutMapping
@RequestMapping(value = "rm03",method = RequestMethod.DELETE)
public ModelAndView requestMapping03(@RequestBody List<String> list) throws IOException {
HashMap<String, User> hashMap = new HashMap<>();
hashMap.put("user",new User("1","1"));
return new ModelAndView("index",hashMap);
}
其他类似@RequestMapping请求注解的注解有
@GetMapping
等价于@RequestMapping(method = RequestMethod.GET)
@PostMapping
等价于@RequestMapping(method = RequestMethod.POST)。
@PutMapping
等价于@RequestMapping(method = RequestMethod.PUT)。
@DeleteMapping
等价于@RequestMapping(method = RequestMethod.DELETE)
总结
@RequestMapping是一个万能请求注解,获取数据时用get请求,删除或者批量删除用delete,新增数据用post,修改数据用put,如果涉及到多参数请求请使用json进行传输。
5.2.2.响应response
响应json数据
通过@ResponseBody注解返回,因为将对象转换为json字符串格式需要用到转换工具,所以需要提前添加上转换工具依赖,推荐使用jackson工具(它不需要手动配置,引入即可。)
导入jackson工具
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.0-rc2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.13.0-rc2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.13.0-rc2</version>
</dependency>
通过@ResponseBody注解将对象转换为json字符串然后返回
@RequestMapping(value = "rm0101",method = RequestMethod.POST)
@ResponseBody //通过
public User requestMapping0101(@RequestBody User user) {
return user;
}
响应数据视图模板
返回ModelAndView,通过模板引擎完成
引入thymeleaf依赖
<!--thymeleaf模板引擎-->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.15.RELEASE</version>
</dependency>
配置thymeleaf模板引擎
@Configuration
public class ViewEngineConfig {
//模板解析器,我这里配置的是thymeleaf模板解析器
@Bean
public SpringResourceTemplateResolver createTemplateResolver(){
SpringResourceTemplateResolver templateResolver
= new SpringResourceTemplateResolver();
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
templateResolver.setCharacterEncoding("utf-8");
templateResolver.setCacheable(false);
templateResolver.setTemplateMode("HTML5");
return templateResolver;
}
//模板引擎,我这里配置的是thymeleaf模板引起
@Bean
public SpringTemplateEngine springTemplateEngine(SpringResourceTemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
//视图解析器,我这里配置的是thymeleaf试图解析器
@Bean
public ThymeleafViewResolver viewResolver(SpringTemplateEngine springTemplateEngine) {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(springTemplateEngine);
viewResolver.setCharacterEncoding("utf-8");
return viewResolver;
}
}
配置返回数据视图模板
@GetMapping("/register")
public ModelAndView register() {
return new ModelAndView("register");
}
其他响应注解
@RestController
等价于@ResponseBody + @Controller合在一起的作用
总结:如果要返回json数据,需要借助转换工具(jackson、fastjson等),如果需要返回视图数据模板需要借助模板引擎(thymeleaf模板引擎、JSP等)。
5.3.文件上传与下载
文件上传和下载是web不可或缺的功能,要实现文件的上传和下载在java中是非常简单的,只需要引入一个通用的文件上传依赖即可实现文件上传,要实现文件下载可以通过设置http的header值来完成。
通过引入通用文件上传依赖commons-fileupload
<!--通用文件上传工具-->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
</dependency>
5.3.1.文件上传
引入commons-fileupload需要配置工具,内容如下
注意:bean的名称必须为multipartResolver,默认情况下@bean的注入的bean名称为方法名称。
@Configuration
public class Config {
@Bean("multipartResolver") //注意该bean的名称必须为multipartResolver
public MultipartResolver createCommonsMultipartResolver(){
CommonsMultipartResolver commonsMultipartResolver
= new CommonsMultipartResolver();
//控制单次最大上传大小为5M
commonsMultipartResolver.setMaxUploadSize(5 * 1024 * 1024);
return commonsMultipartResolver;
}
}
5.3.2.小文件上传
编写contrller控制器方法
如果要接收文件需要使用post来处理,其中produces是指定返回的内容类型,可以不加,如果需要返回字符串的时候可以通过该参数指定返回的内容类型,@RequestPart("myFile")注解必须要使用,用来form表单传输过来的二进制封装到MultipartFile中,文件是否上传完整可以通过前端传入的md5进行校验。
@PostMapping(value = "fileupload",produces = {"application/json;charset=utf-8"})
@ResponseBody
public Map fileUpload(@RequestPart("myFile") MultipartFile myFIle,
String md5,
HttpServletRequest httpServletRequest) throws Exception {
if (myFIle==null){
throw new Exception("MultipartFile不能为空,上传失败");
}
System.out.println(myMD5);
//将文件名从ISO_8859_1编码转换为UTF-8,获取真实文件名
byte[] bytes = myFIle.getOriginalFilename().getBytes(StandardCharsets.ISO_8859_1);
String name = new String(bytes,StandardCharsets.UTF_8);
//获取文件后缀名 .type
String type = name.substring(name.lastIndexOf("."));
//文件类型
String contentType = myFIle.getContentType();
//文件大小
long size = myFIle.getSize();
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy/MM/dd");
String dateNow = now.format(df);
//获取 Web 应用程序在服务器上的实际路径,拼接upload路径,拼接以时间为单位的路径
ServletContext servletContext = httpServletRequest.getServletContext();
String realPath = servletContext.getRealPath("/upload/"+dateNow+"/");
//根据拼接的路径创建目录
File file = new File(realPath);
if (!file.exists()){
file.mkdirs();
}
//用spring框架提供的方法计算md5
String newMD5 = DigestUtils.md5DigestAsHex(new FileInputStream(toFile));
String message="文件上传成功!";
String code="200";
if (!md5.equals(newMD5)){
System.out.println("文件不一致");
message="上传失败,文件已损毁,请重新上传!";
code="500";
throw new Exception("文件md5不一致,文件已损坏!");
}
Map<String,String> map=new HashMap<>();
map.put("code",code);
map.put("message",message);
return map;
//以UUID为名称,防止文件重名
String s = UUID.randomUUID().toString().replace("-", "").toUpperCase();
//拼接上文件后缀
String fileName=file.getPath()+File.separator+s+type;
//将上传的转储到刚才指定的目录
myFIle.transferTo(new File(fileName));
Map<String,String> map=new HashMap<>();
map.put("code","200");
map.put("message","上传成功!");
return map;
}
前端方法
首先需要引入js依赖来计算文件的md5
<script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
对应的input元素
<div class="form-group">
<label>选择要上传的文件</label>
<input type="file" class="form-control-file" id="fileUpload">
</div>
<script>
$(function () {
$("#fileUpload").change(function () {
let files = this.files
for (let i = 0; i < files.length; i++) {
computeMD5(files[i])
}
this.value = ''
})
//计算文件的md5
function computeMD5(file) {
let myMD5;
let fr = new FileReader();
fr.readAsBinaryString(file);
fr.onload = function (e){
let md5 = SparkMD5.hashBinary(e.target.result);
//计算完成后开始上传文件
fileUpload(file,md5)
}
}
//上传文件
function fileUpload(file,md5) {
//构建一个form表单
let formData = new FormData();
formData.append("myFile", file);
formData.append("md5", md5);
$.ajax(
{
url: "http://127.0.0.1:8080/fileupload",
data: formData,
type: "post",
async: "false",
contentType: false, //不要让Jquery处理,否则会导致上传失败
processData: false, //不要让Jquery处理,否则会导致上传失败
success: function (result) {
console.log(result)
},
error: function (result) {
console.log(result)
}
}
);
}
})
</script>
5.3.3.大文件分块上传
实现思路前端将文件进行分块,前端发送分块文件的的md5值、源文件的md5值、是第几个分块,用三个信息来确定服务器中是否包含该分块,,如果服务器中没有该分块,就让前端发起请求上传分块,当所有分块传输完毕后,前端发起一个合并分块请求,将分块合并成一个完整的文件,再对比一下合成的文件的md5值还有合成文件的大小就可以确定文件合并后的文件是否完整了。
编写contrller控制器方法检查分块是否存在。
//检查分片情况
@PostMapping("/checkChunk")
@ResponseBody
public Map checkChunk(String sourceMD5,String chunkMD5,Integer num) throws Exception {
//分块存储位置
String chunkFilePath="D:\\fileTemp\\temp\\chunk\\"+sourceMD5+"\\"+num;
String code="200";
Boolean isExist=true;
String message="已经上传过了";
File chunkFile=new File(chunkFilePath);
//判断分块文件是否存在
if (!chunkFile.exists()){
code="200";
isExist=false; //不存在就反馈给前端,分块不存在,前端根据该值来决定是否上传分块
message="文件未上传";
}else {
//对比分块是否损坏
FileInputStream fis = new FileInputStream(chunkFile);
String newMD5 = DigestUtils.md5DigestAsHex(fis);
if (!chunkMD5.equals(newMD5)){
code="200";
isExist=false; //损坏的分块告知前端,需要重新上传
message="分块已损坏,请重新上传";
}
fis.close();
}
HashMap<String,Object> hashMap = new HashMap<>();
hashMap.put("code",code);
hashMap.put("isExist",isExist);
hashMap.put("message",message);
return hashMap;
}
编写contrller控制器方法上传分块。
@PostMapping(value = "chunkUpload")
@ResponseBody
public Map chunkUpload(@RequestPart("myFile") MultipartFile myFIle,
String sourceMD5,String chunkMD5,Integer num
) throws Exception {
if (myFIle==null){
throw new Exception("MultipartFile不能为空,上传失败");
}
System.out.println(chunkMD5);
//将文件名从ISO_8859_1编码转换为UTF-8,获取真实文件名
byte[] bytes = myFIle.getOriginalFilename().getBytes(StandardCharsets.ISO_8859_1);
String name = new String(bytes,StandardCharsets.UTF_8);
//根据拼接的路径创建目录
String chunkFile="D:\\fileTemp\\temp\\chunk\\"+sourceMD5;
File file = new File(chunkFile);
if (!file.exists()){
file.mkdirs();
}
File chunkPath = new File("\\"+file.getPath() + "\\" + num);
//将分块复制到指定路径
myFIle.transferTo(chunkPath);
//用spring框架提供的方法计算md5
FileInputStream fis = new FileInputStream(chunkPath);
String newMD5 = DigestUtils.md5DigestAsHex(fis);
fis.close();
String message="文件上传成功!";
String code="200";
if (!chunkMD5.equals(newMD5)){
System.out.println("文件不一致");
message="上传失败,文件已损毁,请重新上传!";
code="500";
throw new Exception("文件md5不一致,文件已损坏!");
}
Map<String,String> map=new HashMap<>();
map.put("code",code);
map.put("message",message);
return map;
}
编写contrller控制器方法合并分块。
@PostMapping("/fileMerge")
@ResponseBody
public Map fileMerge(String fileName,String type,String size,String md5) throws Exception {
byte[] bytes = fileName.getBytes(StandardCharsets.ISO_8859_1);
fileName=new String(bytes,StandardCharsets.UTF_8);
HashMap<String, String> map = new HashMap<>();
String message="";
String sourceFilePath="D:\\fileTemp\\temp\\chunk\\"+md5;
String targetFilePath="D:\\fileTemp\\temp\\merge\\"+md5;
String fileMerged=targetFilePath+"\\"+fileName;
File sourceFile=new File(sourceFilePath);
File targetFile=new File(targetFilePath);
File mergeFile = new File(fileMerged);
if (!sourceFile.exists()){
throw new Exception("分块文件不存在,请重新上传!");
}
if (!targetFile.exists()){
targetFile.mkdirs();
}
//检查文件是否已经合并过了
if (mergeFile.exists()){
FileInputStream fis = new FileInputStream(mergeFile);
String newMD5 = DigestUtils.md5DigestAsHex(fis);
if (md5.equals(newMD5)&&(Long.parseLong(size)==mergeFile.length())){
message="文件已经合并完成了,不用多次合并";
}
fis.close();
}else {
File[] files = sourceFile.listFiles();
//对分块排序,保证数据完整性
Arrays.sort(files,(File f1,File f2)->{
return (Integer.valueOf(f1.getName())-Integer.valueOf(f2.getName()));
});
//合并文件
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(mergeFile));
for (File file : files) {
System.out.println(file.getName());
FileInputStream fis = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(fis);
int len=-1;
byte[] bits=new byte[1*1024*1024];
while ((len=bis.read(bits))!=-1){
bos.write(bits,0,len);
}
fis.close();
bis.close();
bos.flush();
System.out.println("完成 "+file.getName()+" 文件合并");
}
bos.close();
message="全部文件合并完成!路径为:"+mergeFile.getPath();
System.out.println("全部文件合并完成!路径为:"+mergeFile.getPath());
}
map.put("code","200");
map.put("message",message);
return map;
}
前端的js控制,这里没有对这些函数封装,其实可以写一个函数来封装ajax请求,让代码重复利用。
<div class="form-group">
<label>选择要上传的文件</label>
<input type="file" class="form-control-file" id="fileUpload">
</div>
<script>
$(function () {
let chunkSize = 2 * 1024 * 1024;
let sourceMD5 = "";
let fileName = "";
let type = "";
let size = "";
let allChunks = 0;
let chunkIndex = 0;
$("#fileUpload").change(function () {
let files = this.files
sourceMD5 = "";
for (let i = 0; i < files.length; i++) {
let file = files[i]
console.log(file)
fileName = file.name; //源文件名称
size = file.size //源文件大小
type = file.type //源文件类型
computeMD5(file); //计算源文件md5值
}
this.value = ''
})
//计算文件的md5值,
function computeMD5(file, num) {
let fr = new FileReader();
fr.readAsBinaryString(file);
fr.onload = function (e) {
console.log("正在计算MD5值")
let md5 = SparkMD5.hashBinary(e.target.result);
console.log(sourceMD5)
if (sourceMD5 === "") {
sourceMD5 = md5;
//对源文件进行分块
chunkFile(file, chunkSize, false);
} else {
//检查服务器端分块是否存在
checkChunk(sourceMD5, md5, num, file)
}
}
}
//对源文件进行分块,大小设置的是2M=2*1024*1024
function chunkFile(file, chunkSize) {
const chunks = Math.ceil(file.size / chunkSize);
allChunks = chunks;
console.log(allChunks)
const chunksList = [];
let currentChunk = 0;
while (currentChunk < chunks) {
const start = currentChunk * chunkSize;
const end = Math.min(file.size, start + chunkSize);
const chunk = file.slice(start, end);
// chunksList.push(chunk);
computeMD5(chunk, currentChunk);
currentChunk++;
}
}
//合并分块,将源文件名称、类型、大小、md5值上传到服务端
function fileMerge(data) {
let fromData = new FormData();
fromData.append("fileName", data.fileName)
fromData.append("type", data.type)
fromData.append("size", data.size)
fromData.append("md5", data.md5);
$.ajax(
{
url: "http://127.0.0.1:8080/fileMerge",
data: fromData,
type: "post",
contentType: false,
processData: false,
success: function (result) {
console.log(result)
},
error: function (result) {
console.log(result)
}
}
);
}
//对分块文件进行检查,只对服务器不存在的分块进行上传
function checkChunk(mySourceMD5, chunkMD5, num, file) {
let formData = new FormData();
formData.append("sourceMD5", sourceMD5);
formData.append("chunkMD5", chunkMD5);
formData.append("num", num);
$.ajax(
{
url: "http://127.0.0.1:8080/checkChunk",
data: formData,
type: "post",
async: "false",
timeout: 3000,
contentType: false,
processData: false,
success: function (result) {
console.log(result)
if (result.isExist === false) {
chunkUpload(file, mySourceMD5, chunkMD5, num, 0)
} else {
chunkIndex++
if (chunkIndex === allChunks) {
chunkIndex = 0;
console.log("合并请求")
let data = {
fileName: fileName,
type: type,
size: size,
md5: sourceMD5
}
fileMerge(data)
sourceMD5 = "";
}
}
},
error: function (result) {
console.log(result)
}
}
);
}
//上传分块,5秒超时,失败重试三次
function chunkUpload(myfile, mySourceMD5, chunkMD5, num, retryCount) {
console.log("上传第:" + num + " 个分块");
let formData = new FormData();
formData.append("myFile", myfile);
formData.append("sourceMD5", mySourceMD5);
formData.append("chunkMD5", chunkMD5);
formData.append("num", num);
//当传输二进制的表单时,contentType: false和processData: false必须要设置
$.ajax(
{
url: "http://127.0.0.1:8080/chunkUpload",
data: formData,
type: "post",
async: "false",
timeout: 5000, //设置请求超时5秒
contentType: false,
processData: false,
success: function (result) {
console.log(result)
console.log(chunkIndex)
chunkIndex++;
if (chunkIndex === allChunks) {
chunkIndex = 0;
console.log("发起合并请求")
let data = {
fileName: fileName,
type: type,
size: size,
md5: sourceMD5
}
fileMerge(data)
sourceMD5 = "";
}
},
error: function (result) {
console.log(result)
let f = formData.get("myFile")
let s = formData.get("sourceMD5");
let c = formData.get("chunkMD5");
let n = formData.get("num");
//失败重试
if (result.statusText === "timeout") {
if (retryCount < 3) {
retryCount++
console.log("请求超时!重新发起请求。" + retryCount + "次")
chunkUpload(f, s, c, n, retryCount)
} else {
console.log("重试失败次数过多。。。停止重试")
}
}
}
}
);
}
})
</script>
5.3.4.文件下载
文件下载是一般来说小文件,例如图片、js、css这些小文件就直接将资源路径开放出去就可以了,通过配置放行这些资源,例如下面的配置类。通过实现WebMvcConfigurer的addResourceHandlers方法放行小文件即可。
@ComponentScan(value = "gc.learn.*")
@EnableWebMvc
@EnableAspectJAutoProxy//开启AOP代理
public class WebMvcConfig implements WebMvcConfigurer {
/*
* 开启对静态资源的访问
* 类似在Spring MVC的xml配置文件中设置<mvc:default-servlet-handler/>元素
*/
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
//放行静态资源,例如:图片、js、css等
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//将网络请求路径/static/** 映射到本地文件路径/WEB-INF/static/
registry.addResourceHandler("/static/**")
.addResourceLocations("/WEB-INF/static/");
}
@Bean("multipartResolver")
public MultipartResolver createCommonsMultipartResolver(){
CommonsMultipartResolver commonsMultipartResolver
= new CommonsMultipartResolver();
commonsMultipartResolver.setMaxUploadSize(5 * 1024 * 1024);
return commonsMultipartResolver;
}
}
5.3.5.大文件分片下载
下载所谓的分片并不需要将一个大文件真实的分块存储存储在下载,而是通过更改文件指针指向,返回前端所需要的那一段文件内容就好。通过文件随机读写对象RandomAccessFile的seek方法设置文件指针,也可以通过BufferedInputStream对象的skip方法设置文件指针移动,当然最重要的还是要设置htt协议响应的下载请求头,只有这样才能触发浏览器的下载操作。
@GetMapping("/download")
public void fileDownload(HttpServletRequest request, HttpServletResponse response) throws IOException {
String filePath="F:\\a\\b\\c.zip";
//得到文件对象
File file = new File(filePath);
//得到文件大小
long fileSize=file.length();
//得到文件名称
String fileName = URLEncoder.encode(file.getName(),"UTF-8");
//设置http协议中相应标头,这些都可以在http协议官方文档中查到
//Content-Type表示响应的内容为二进制下载数据
response.setContentType("application/x-download");
//Content-Disposition 表示响应的内容该以保存的形式呈现(弹出下载对话框)
response.setHeader("Content-Disposition","attachment; filename=\""+fileName+"\"");
//Accept-Ranges,向客户端表明支持分块下载
response.setHeader("Accept-Ranges","bytes");
response.setCharacterEncoding("UTF-8");
//通过自定义标头将文件名称和文件大小返回给前端(这里只能这样返回给前端,因为响应体是文件二进制)
response.setHeader("fileSize",fileSize+"");
response.setHeader("fileName",fileName);
//设置分片信息
//如果前端下载支持分片下载就会在请求头中设置一个叫Range的请求头,表示请求从起始位置和结束位置这之间的文件内容
//Range的请求头值为bytes=0-1000或者0-,它表示的是请求文件的0到1000字节的下标位置的部分文件或者0到整个文件末尾完的部分。
//分别表示起始位置,结束位置,总共已经读取了多少字节(用于判断文件是否读取完了)
long start=0,end=fileSize-1,sum=0;
//获取请求头中的Range
String range = request.getHeader("Range");
RandomAccessFile raf=null;
BufferedOutputStream os = null;
try {
//判断标头range是否存在,并解析出起始位置和结束位置
if (range!=null&&(!"".equals(range))){
String[] rangeValue = range.substring(range.indexOf("=")).split("-");
if (rangeValue.length==2){
start= Long.parseLong(rangeValue[0]);
end= Long.parseLong(rangeValue[1]);
}else {
start=Long.parseLong(rangeValue[0]);
}
}
//要读取的字节数数量
byte[] bytes = new byte[2*1024*1024];
//请求的总字节长度
long rangeSize=end-start+1;
//读取的长度
int length = 0;
//Content-Range HTTP 响应标头表示部分消息在完整消息中的位置。Content-Range: bytes 200-1000/67589
String contentRange="bytes "+start+"-"+end+"/"+fileSize;
response.setHeader("Content-Range",contentRange);
//HTTP Content-Length 标头表示发送给接收方的消息体的大小(以字节为单位)。
response.setHeader("Content-Length",rangeSize+"");
//字节输出流,输出源为http响应字节流
os = new BufferedOutputStream(response.getOutputStream());
//随机读取流,可以通过seek方法设置文件指针,指针下标从0开始
raf = new RandomAccessFile(file,"rw");
raf.seek(start);
//读取不超过请求的总字节数
while (sum<rangeSize){
length = raf.read(bytes);
sum+=length;
if (length!=-1){
os.write(bytes,0,length);
}
}
System.out.println("文件下载完成!");
}catch (Exception e){
System.out.println("文件下载出错!");
e.printStackTrace();
}finally {
os.close();
raf.close();
}
}
5.3.6.流媒体
实现了服务器的大文件的下载,其实就已经解决了流媒体了,无非就是前端将获取的视频片段进行播放,当然使用fmpga工具
TODO
5.4.Filter过滤器
目前spring的web容器中存在两个容器,一个是Servlet容器,另一个是Spring的ioc容器,这两个容器是互不影响且不知道对方的存在,就是导致了一种情况,那就是当我们需要在,filter过滤器中注入ico容器中的bean时就会出现问题,因为filter是在servlet容器中运行的,所以直接注入是不行的,为了解决这个问题,springmvc专门设计了DelegatingFilterProxy类。过滤器可以用于鉴权认证的等信息。
DelegatingFilterProxy类
编写一个filter,这里使用的是spring中的@Component注解,并注入UserService
@Component("myFilter")
public class MyFilter implements Filter {
@Autowired
UserService userService;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("MyFilter");
System.out.println(userService);
chain.doFilter(request,response);
}
}
在servlet配置配置类中重写getServletFilters方法
//继承AbstractDispatcherServletInitializer类,
//当Web服务器启动时会调用继承过来的createServletApplicationContext方法
public class ServletConfig extends AbstractDispatcherServletInitializer {
//当Web服务器启动时该方法会最先调用,主要用来加载spring的ioc容器
@Override
protected WebApplicationContext createServletApplicationContext() {
//AnnotationConfigWebApplicationContext相当于Spring的Ioc容器,
AnnotationConfigWebApplicationContext webApplicationContext
= new AnnotationConfigWebApplicationContext();
//通过register方法加载配置类
webApplicationContext.register(WebMvcConfig.class);
return webApplicationContext;
}
//当前web服务其启动完成后会调用该方法,该方法可以用来配置filter过滤器和Listener监听器
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
//创建一个字符编码过滤器,处理所有请求的字符编码
CharacterEncodingFilter characterEncodingFilter
= new CharacterEncodingFilter();
characterEncodingFilter.setEncoding("utf-8");
//将过滤器添加到servlet运行容器中
FilterRegistration.Dynamic filterRegistration
= servletContext.addFilter("characterEncodingFilter", characterEncodingFilter);
//设置对哪些请求进行拦截过滤
filterRegistration.addMappingForServletNames(
EnumSet.of(DispatcherType.REQUEST,DispatcherType.INCLUDE,DispatcherType.FORWARD),
false,"/*");
}
//使用DelegatingFilterProxy代理实例化filter
@Override
protected Filter[] getServletFilters() {
DelegatingFilterProxy filterProxy = new DelegatingFilterProxy();
filterProxy.setTargetBeanName("myFilter");
DelegatingFilterProxy filterProxy2 = new DelegatingFilterProxy();
filterProxy2.setTargetBeanName("myFilterServlet");
return new Filter[]{filterProxy,filterProxy2};
}
//配置servlet拦截所有请求
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
}
5.5.Interceptor拦截器
Filter由Servlet容器管理,而Interceptor是IOC容器管理,他们都可以对请求进行过滤,但是作用范围是不同的,filter是拦截范围包括了Interceptor,Interceptor的拦截范围是Controller方法,因为Interceptor受ioc管理,所以要在Interceptor中注入bean就可以直接使用。
使用Interceptor拦截器
一个Interceptor拦截器必须实现HandlerInterceptor。
@Order(1)
@Component
public class MyInterceptor implements HandlerInterceptor {
@Autowired
UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("在controller方法前执行");
System.out.println(userService);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("在controller方法正常返回后完后执行");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("不管controller是否异常都会执行");
}
}
在springmvc配置类中注册拦截器
@Configuration
@ComponentScan(value = "gc.learn.*")
@EnableWebMvc
@EnableWebSocket
@EnableAspectJAutoProxy//开启AOP代理
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
HandlerInterceptor[] interceptors; //注入所有的拦截器
@Override //注册拦截器
public void addInterceptors(InterceptorRegistry registry) {
for (HandlerInterceptor interceptor : interceptors) {
registry.addInterceptor(interceptor);
}
}
}
5.6.WebSocket长连接
WebSocket是一种基于HTTP的长链接技术。传统的HTTP协议是一种请求-响应模型,如果浏览器不发送请求,那么服务器无法主动给浏览器推送数据。如果需要定期给浏览器推送数据,例如股票行情,或者不定期给浏览器推送数据,例如在线聊天,基于HTTP协议实现这类需求,只能依靠浏览器的JavaScript定时轮询,效率很低且实时性不高,而通过WebSocket能保持服务器和浏览器端的长时间连接,从而完成服务器主动向浏览器推送信息。
对于WebSocket的http连接请求对应的http请求头内容如下
GET /chat HTTP/1.1
Host: www.example.com
Upgrade: websocket
Connection: Upgrade
而服务端相应WebSocket连接的http响应头内容如下
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
收到成功响应后表示WebSocket“握手”成功,这样,代表WebSocket的这个TCP连接将不会被服务器关闭,而是一直保持,服务器可随时向浏览器推送消息,浏览器也可随时向服务器推送消息。双方推送的消息既可以是文本消息,也可以是二进制消息,一般来说,绝大部分应用程序会推送基于JSON的文本消息。
使用WebSocket
引入websocket依赖
<!--websocket依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>5.2.12.RELEASE</version>
</dependency>
<!--tomcat的websocket-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>9.0.30</version>
</dependency>
配置websocket
开启EnableWebSocket支持、
@Configuration
@ComponentScan(value = "gc.learn.*")
@EnableWebSocket //开启EnableWebSocket支持、
public class WebMvcConfig implements WebMvcConfigurer {
//创建WebSocketConfigurer配置,添加ChatHandlerWebSocket处理类和处理类对应的拦截器
//ChatHandshakeInterceptor拦截器将session中自定义的属性和属性值复制到WebSocketSession中
//TokenHandshakeInterceptor拦截器允许前端通过WebSocket子协议传递token
@Bean
WebSocketConfigurer createWebSocketConfigurer(
@Autowired ChatHandlerWebSocket chatHandler,
@Autowired ChatHandshakeInterceptor chatInterceptor,
@Autowired TokenHandshakeInterceptor tokenHandshakeInterceptor)
{
return new WebSocketConfigurer() {
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 把URL与指定的WebSocketHandler关联,可关联多个:
registry.addHandler(chatHandler, "/chat").addInterceptors(chatInterceptor).addInterceptors(tokenHandshakeInterceptor);
}
};
}
}
ChatHandlerWebSocket处理器,用来处理WebSocket请求
@Component
public class ChatHandlerWebSocket extends TextWebSocketHandler {
@Autowired
ChatHistory chatHistory;
//存储所有已经连接的websocket的客户端信息
private Map<String, WebSocketSession> clients=new ConcurrentHashMap<>();
//websocket连接后调用该方法
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
Map<String, Object> attributes = session.getAttributes();
//获取前端通过WebSocket子协议传递过来的token
String token = session.getHandshakeHeaders().get("sec-websocket-protocol").get(0);
//获取的是通过session复制的属性和值user
User user = (User)attributes.get("user");
System.out.println("用户:"+user.getName()+" 连接成功!" +" token:"+token);
//将当前WebSocketSession存储到连接池中
clients.put(session.getId(),session);
ObjectMapper objectMapper = new ObjectMapper();
//发送历史消息
TextMessage textMessage = new TextMessage(objectMapper.writeValueAsString(chatHistory.getHistory()));
session.sendMessage(textMessage);
}
//断开WebSocket连接时调用该方法
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.out.println("sessionId:"+session.getId()+" 断开连接!");
clients.remove(session.getId());
}
//接收前端发过来的WebSocket消息
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String s = message.getPayload();
if (s.isEmpty()) {
return;
}
User user = (User) session.getAttributes().get("user");
ObjectMapper objectMapper = new ObjectMapper();
ChatMessage chatMessage = objectMapper.readValue(s, ChatMessage.class);
System.out.println(user.getName()+"发来消息:"+chatMessage.text);
//加入历史消息
chatHistory.addToHistory(chatMessage);
ChatMessage chatMessage1 = new ChatMessage();
chatMessage1.text="消息已经收到";
chatMessage1.name="system";
chatMessage1.timestamp= Instant.now().toEpochMilli();
chatHistory.addToHistory(chatMessage1);
TextMessage textMessage = new TextMessage(objectMapper.writeValueAsString(chatHistory.getHistory()));
session.sendMessage(textMessage);
}
}
ChatHandshakeInterceptor拦截器将session中自定义的属性和属性值复制到WebSocketSession中
@Component
public class ChatHandshakeInterceptor extends HttpSessionHandshakeInterceptor {
public ChatHandshakeInterceptor(){
super(Arrays.asList("user"));
}
}
TokenHandshakeInterceptor拦截器允许前端通过WebSocket子协议传递token
@Component
public class TokenHandshakeInterceptor implements HandshakeInterceptor {
@Override //握手前
public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
return true;
}
@Override //握手后
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
HttpServletRequest httpRequest = ((ServletServerHttpRequest) request).getServletRequest();
HttpServletResponse httpResponse = ((ServletServerHttpResponse) serverHttpResponse).getServletResponse();
if (httpRequest.getHeader("Sec-WebSocket-Protocol") != null) {
//添加Sec-WebSocket-Protocol响应头,否则会导致子协议的握手失败
httpResponse.addHeader("Sec-WebSocket-Protocol", httpRequest.getHeader("Sec-WebSocket-Protocol"));
}
}
}
前端控制器,返回页面,设置一个session属性user,ChatHandshakeInterceptor拦截器会将该属性和对应值复制到WebSocketSession中
@GetMapping("/chatRoom")
public ModelAndView chatRoom(HttpServletRequest request, HttpServletResponse responserespo){
User user = new User("AL", "666666");
HttpSession session = request.getSession();
session.setAttribute("user",user);
return new ModelAndView("/chatRoom");
}
<div th:replace="~{base :: html(head = '聊天室',content = ~{::content})}">
<th:block th:fragment="content">
<div id="chatTemplate" style="display:none">
<div class="mb-1">
<p>
<span class="chat-name font-weight-bold"></span>:
<br>
<span class="chat-message"></span>
<br>
<span class="chat-timestamp text-muted" style="font-size:0.8em"></span>
</p>
</div>
</div>
<h3>Chat Room</h3>
<div id="chatRoom" class="overflow-auto border border-info rounded p-3" style="height:480px">
<div id="connecting">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
<span>Connecting...</span>
</div>
</div>
<form id="chatForm" onsubmit="return sendChatMessage()">
<div class="form-group">
<label>Message:</label>
<input type="text" class="form-control" max-length="100">
</div>
<button type="submit" class="btn btn-primary" disabled>Submit</button>
</form>
</th:block>
</div>
<script>
function sendChatMessage() {
var input = $('#chatForm input[type=text]');
var s = input.val();
s = $.trim(s);
if (s) {
console.log('send: ' + s);
let data={
name:"test",
text: s,
timestamp:new Date().valueOf()
}
window.chatWs.send(JSON.stringify(data));
input.val('');
}
return false;
}
function appendMessage(msg) {
console.log(msg)
var templ = $('#chatTemplate').html();
var div = $('<div></div>');
div.html(templ);
div.find('.chat-name').text(msg.name);
div.find('.chat-timestamp').text(new Date(msg.timestamp).toLocaleString());
div.find('.chat-message').text(msg.text);
var room = $('#chatRoom');
room.append(div);
room.scrollTop(room.prop('scrollHeight') - room.height());
}
$(function () {
console.log(location.host)
let token=['hdjkaskjdahsdkahsdajdsad'] //WebSocket子协议携带token值
let ws = new WebSocket('ws://' + location.host + '/chat',[token]);
ws.addEventListener('open', function (event) {
$('#connecting').hide();
$('#chatForm button[type=submit]').removeAttr('disabled');
});
ws.addEventListener('message', function (event) {
console.log('message: ' + event.data);
var msgs = JSON.parse(event.data);
for (msg of msgs) {
appendMessage(msg);
}
});
ws.addEventListener('close', function () {
console.log("WebSocket" + " 被关闭了")
$('#chatForm button[type=submit]').attr('disabled', 'disabled');
});
window.chatWs = ws;
});
</script>
运行效果如下:

总结:通过websocket可以实现聊天室功能,实现实时通信,也可以用于服务端主动推送消息到客户端。