Java Web后端基础
第1章 Maven项目
1.1 Maven简介
Maven基于项目对象模型,通过一小段描述信息来管理项目的构建、报告和文档。
Maven提供了一套标准化的项目结构、构建流程和一套依赖管理机制。
Maven模型:pom.xml→项目对象模型↔依赖管理模型→仓库
Maven仓库包含本地仓库、中央仓库、远程仓库(私服)。
Maven项目获取jar包时,首先在本地仓库寻找是否有对应jar包,若没有则先访问远程仓库;远程仓库若没有,则访问中央仓库。
1.2 Maven常用命令
常用命令如下。
mvn compile # 编译Maven项目,生成target目录
mvn clean # 删除target目录
mvn package # 将项目打包成jar包或war包
mvn test # 编译运行Maven项目中的test目录下的测试文件
mvn install # 将当前项目安装到本地仓库
1.3 Maven生命周期
Maven对项目构建的生命周期划分为3套:clean、default、site。
clean:清理工作,执行顺序是pre-clean→clean→post-clean。
default:核心工作,执行顺序是compile→test→package→install。
site:产生报告、发布站点,执行顺序是pre-site→site→post-site。
运行后面的命令,前面的所有命令会自动执行。
1.4 Maven坐标
Maven中的坐标是资源的唯一标识。使用坐标来定义项目或引入项目中需要的依赖。
-
Maven坐标的主要组成
groupId
:当前Maven项目隶属组织名称。artifactId
:当前Maven项目名称。version
:项目版本号。示例:
<groupId>org.example</groupId> <artifactId>JavaWebLearning</artifactId> <version>1.0-SNAPSHOT</version> <!--依赖范围,在该范围内有效--> <scope>test</scope>
第2章 数据库连接池
在这里使用Druid。
2.1 Druid配置文件及基本使用
Druid配置文件如下:
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql:///mybatis_train?useSSL=false&useServerPrepStmts=true
username=root
password=123456
# 初始化连接数量
initialSize=5
# 最大连接数
maxActive=10
# 最大等待时间
maxWait=3000
Druid使用步骤如下:
// 1. 加载配置文件
Properties prop = new Properties();
prop.load(DruidTrainDemo.class.getResource("druid.properties").openStream());
// 2. 获取连接池对象
DataSource dataSource = DruidDataSourceFactory.createDataSource(prop);
// 3. 获取数据库连接对象
Connection conn = dataSource.getConnection();
// 4. 操作Connection对象
System.out.println(conn);
第3章 MyBatis
MyBatis是一款用于简化JDBC开发的持久层框架。
Java EE有3层架构:表现层、业务层、持久层。
持久层:将数据保存到数据库的那一层代码。
3.1 MyBatis与JDBC
JDBC的缺点有:硬编码、操作繁琐。
硬编码:将注册信息、SQL语句等直接写入代码。
MyBatis将原来硬编码的内容写入配置文件中,方便加载。
3.2 MyBatis配置
首先,将MyBatis的依赖导入Maven的pom.xml文件中:
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<!--version中的字段改为自己用的版本号-->
<version>x.x.x</version>
</dependency>
随后,在resources文件夹中创建mybatis-config.xml,并写入以下内容:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<!--MySQL的Java驱动-->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<!--连接MySQL数据库的URL-->
<property name="url" value="jdbc:mysql:///mybatis?useSSL=false"/>
<!--MySQL服务器的登录信息-->
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--加载SQL映射文件-->
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>
3.3 MyBatis查询表中数据
查询表中数据的操作流程如下。
-
建表、添加数据。
-
创建模块,导入坐标。
-
编写MyBatis配置文件。
-
编写SQL映射文件。
-
编码。
SQL映射文件,配置如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--命名空间-->
<mapper namespace="test">
<!--resultType处改为期望返回的结果集类型-->
<select id="selectUser" resultType="User">
select * from user;
</select>
</mapper>
MyBatis查询编码操作如下:
// 1. 根据配置文件获取SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
inputStream.close();
// 2. 获取SqlSession对象,执行SQL语句
SqlSession sqlSession = sqlSessionFactory.openSession();
// 3. 执行SQL
List<User> users = sqlSession.selectList("selectUserAll");
// 4. 处理结果集
System.out.println(users);
// 5. 释放资源
sqlSession.close();
3.4 Mapper代理开发
Mapper代理开发的流程如下。
- 定义与SQL映射文件同名的Mapper接口,并将Mapper接口和SQL映射文件放置在同一个目录下。
注:在src文件夹下创建一个xxx.yyy目录,代码放里面;在resources文件夹下创建一个xxx/yyy目录,同名配置文件放里面。
- 设置SQL映射文件的namespace属性为Mapper全限定名。
即:<mapper namespace="xxx.yyy.testclass">
-
在Mapper接口下定义方法,方法名为SQL映射文件中SQL语句的ID,保持参数类型与返回值一致。
-
编码。
编码内容较2.3节,在执行SQL处有些许改动:
// 获取UserMapper接口的代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class); // 根据接口UserMapper生成一个查询类
List<User> users = userMapper.selectUserAll();
当有众多查询类时,可以使用包扫描的方式进行配置。
语句:
<package name="com.train"/>
3.5 MyBatis核心配置文件
MyBatis官方文档可以参考这里。
以下是内容详解。
-
环境配置
考虑到MyBatis可能要配置多个数据源,因此可以在environments标签下配置多个environment。
示例:
<environments default="development"> <!--开发库--> <environment id="development"> <transactionManager type="JDBC"> <property name="..." value="..."/> </transactionManager> <dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource> </environment> <!--测试库--> <environment id="test"> <transactionManager type="JDBC"> <property name="..." value="..."/> </transactionManager> <dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource> </environment> </environments>
-
类型别名
类型别名可以用于简化全限定名的书写。
在mybatis-config.xml文件的configuration标签内加入该选项,之后的映射文件就不再需要写全限定名。
<typeAliases> <package name="com.train"/> </typeAliases>
注意,configuration标签内的字段排列顺序如上述官网所示。
3.6 MyBatisX插件
MyBatisX是一款基于IDEA的快速开发插件,有以下功能:
XML和接口相互跳转、根据接口方法生成statement。
3.7 常见问题处理
-
数据库字段名与类名不一致
根据数据库字段和Java类属性命名规则,经常出现数据库字段和类属性名不一致的情况,此时不能自动映射,获取到的类对应值为null。
解决方案:SQL片段(略)、ResultMap。
ResultMap的使用方法:
-
在
select
标签前使用resultMap
标签; -
将
select
标签中的resultType
属性替换为resultType
属性。
ResultMap示例:
<!--使用ResultMap将不兼容的字段匹配起来--> <resultMap id="BrandResultMap" type="Brand"> <!--这里可以写2种属性,id属性用于主键字段,result属性用于一般字段--> <result column="brand_name" property="brandName"/> <result column="company_name" property="companyName"/> </resultMap> <!--在这里,数据库字段为下划线命名,Java类属性为驼峰命名,两者不兼容,注意,这里不用resultType,而是用resultMap--> <select id="selectAll" resultMap="BrandResultMap"> select id, brand_name, company_name, ordered, description, status from brands; </select>
-
-
含参查询处理
有的时候需要传入参数,然后进行查询等操作,处理方法如下。
在Mapper中设置带参查询方法:
Brand selectById(int id);
在映射文件中设置SQL语句:
<!--这里的#{id}要与Mapper中的参数同名--> <select id="selectById" resultMap="BrandResultMap"> select id, brand_name, company_name, ordered, description, status from brands where id = #{id}; </select>
传参时,参数有2种写法:
#{var}
:将其替换为?
,可以避免SQL注入。${var}
:拼接SQL语句,存在SQL注入漏洞,一般在表名或列名不固定的时候使用。注意:上述事例中,
select
标签中有一个属性parameterType="int"
被省略掉了,这里可写可不写。遇到特殊符号,如
<
号时,在XML中可能会出现格式冲突,处理方法有:使用转义字符:
<
→ \rightarrow →<
。CDATA区:
<![CDATA[<]]>
。
3.8 添加操作
-
insert
操作的步骤-
编写Mapper接口方法
参数为除了
id
以外的所有数据。方法名:
void insert_xxx(Brand brand);
-
在SQL映射文件中编写SQL语句。
示例:
<!--useGeneratedKeys="true" keyProperty="id"两个属性表示执行完插入之后,主键会返回到类中--> <insert id="add" useGeneratedKeys="true" keyProperty="id"> insert into brands (brand_name, company_name, ordered, description, status) values (#{brandName}, #{companyName}, #{ordered}, #{description}, #{status}); </insert>
-
执行方法
执行方法的Java代码如下:
SqlSession sqlSession = sqlSessionFactory.openSession(); // 参数可以写true,设置自动提交事务 BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class); try { brandMapper.add(new Brand(0, "原神", "米哈游", 10, "你说的对,但是《原神》是由米哈游自主研发的一款全新开放世界冒险游戏。游戏发生在一个被称作「提瓦特」的幻想世界, 在这里,被神选中的人将被授予「神之眼」,导引元素之力。你将扮演一位名为「旅行者」的神秘角色,在自由的旅行中邂逅性格各异、能力独特的同伴们,和他们一起击败强敌,找回失散的亲人——同时,逐步发掘「原神」的真相。", 1)); sqlSession.commit(); } catch (Exception e) { sqlSession.rollback(); } finally { sqlSession.close(); }
-
3.9 删除操作
-
单个删除操作
-
在Mapper接口中编写方法:
void deleteById(int id)
。 -
编写SQL映射文件:
<delete id="deleteById"> delete from brands where id = #{id}; </delete>
-
-
批量删除
-
编写接口方法:
void deleteByIds(@Param("ids")int[] ids);
-
在SQL映射文件中使用foreach标签:
<!--注意,这里的collection是一个Map集合,原名为array,若想换为ids,需要在接口方法使用@Param("ids")--> <delete id="deleteByIds"> delete from brands where id in <foreach collection="ids" item="id" separator="," open="(" close=")">#{id}</foreach>; </delete>
-
3.10 MyBatis参数传递
MyBatis底层使用了ParamNameResolver
类封装参数。
MyBatis的Mapper中的方法传入多个参数时,会将其封装为一个map
集合:
map.put("arg0", val1);
map.put("param1", val1);
map.put("arg1", val2);
map.put("param2", val2);
要求arg0
这样的参数名与SQL映射文件中设置的参数名一致。
单个参数时:
-
POJO类:直接使用,属性名和参数占位符名称一致。
-
Map集合:直接使用,键名和参数占位符名称一致。
-
Collection:封装为Map集合。
map.put("arg0", collection_set); map.put("collection", collection_set);
-
List:
map.put("arg0", list); map.put("collection", list); map.put("list", list);
-
Array:与Collection类似。
-
其它类型:直接使用。
注意:需要封装的类型,尽量使用@Params("typename")
注解,以方便阅读。
3.11 使用注解完成增删改查
一般用于完成简单功能,复杂功能仍然用配置文件完成。
示例:
@Select("select id, brand_name, company_name, ordered, description, status from brands;")
List<Brand> selectAll();
@Select("select id, brand_name, company_name, ordered, description, status from brands where id = #{id};")
Brand selectById(int id);
第4章 JSP
4.1 JSP概念
Java Server Pages,用于简化Servlet的开发。
JSP = HTML + Java。
JSP本质上就是一个Servlet。
JSP编译过程:jsp→jsp.java(Servlet类型)→jsp.class
4.2 JSP快速入门
-
导入JSP坐标
<dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.2.1-b03</version> <!--避免重复导入--> <scope>provided</scope> </dependency>
-
JSP脚本
JSP可以看做一个Java Servlet文件直接打印了HTML语句。明显的HTML代码由
out.print()
语句打印输出,Java代码由后端执行。JSP有3个原始标签,分别是:
<% %>
:放入_jspService()
方法中直接执行。<%=var%>
:放入out.print()
中,作为打印的参数。<%! %>
:放到_jspService()
方法之外,被类直接包含。 -
JSP缺点
JSP缺点有:书写、阅读麻烦,复杂度高,占空间,难以调试,不利于团队协作等问题。
如今,HTML5+Ajax已经替代了JSP的作用。
4.3 EL表达式
EL(Express Language)用于简化JSP代码。
示例:${var}
。
EL表达式用于从域对象中获取数据,功能类似getAttribute()
方法。
-
域对象
Java Web中有4大域对象:
-
page:当前页面有效。
-
request:当前请求有效。
-
session:当前会话有效。
-
application:当前应用有效。
-
EL表达式会依次从范围小到大寻找,直到找到为止。
4.4 JSTL标签
JSTL(JSP Standard Tag Library),使用标签取代JSP的Java代码。
示例:
<c:if test="${flag}==1">
男
</c:if>
<c:if test="${flag}==2">
女
</c:if>
-
前置准备
首先在pom.xml中导入以下依赖:
<dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>taglibs</groupId> <artifactId>standard</artifactId> <version>1.1.2</version> </dependency>
然后在JSP中加入以下字段:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-
forEach
标签示例:
<c:forEach items="brands" var="brand"> <tr> <td>${brand.getId()}</td> <td>${brand.getBrandName()}</td> <td>${brand.getCompanyName()}</td> <td>${brand.getOrdered()}</td> <td>${brand.getDescription()}</td> <td>${brand.getStatus()}</td> </tr> </c:forEach>
但是,在这之前,需要先将
brand
属性写入request请求中,然后转发请求至JSP。这一步在Servlet中进行:req.setAttribute("brands", brands); // 将所需打印的数据存入request对象中 req.getRequestDispatcher("/jstl.jsp").forward(req, resp);
varStatus
属性:用来表示序号。在上述forEach
标签中插入该字段即可。定义了varstatus="xxx"
后,用${xxx.index}
表示从1开始的序号,用${xxx.id}
定义从0开始的序号。此外,还有其它形式的
forEach
标签:<c:forEach begin="0" end="10" step="1" var="i"> ${i} </c:forEach>
第5章 MVC模式和3层架构
-
MVC模式
MVC是一种分层开发模式,其中:
M(Model):业务模型,处理业务。
V(View):视图,界面展示。
C(Controller):控制器,处理请求,调用模型和视图。
调用模型:浏览器访问控制器→控制器调用模型获取数据→控制器将数据交给视图→视图向浏览器响应数据。
-
3层架构
数据访问层:对数据库进行CRUD基本操作。框架有Spring MVC和Struts2。
业务逻辑层:对业务进行封装。框架有Spring。
表现层:接收请求、封装数据、响应数据。框架有MyBatis和Hibername。
SSM:Spring MVC、Spring和MyBatis。
-
二者的不同
MVC模式和3层架构有相似的地方,但又有所不同。
第6章 Spring框架
Spring框架可以整合诸多框架,降低开发复杂度。
Spring的核心思想有2条:IoC控制反转、AoP面向切面。
6.1 IoC和DI
Spring框架包含若干元件:核心容器、AOP、数据访问和数据集成、Web开发、测试。
-
IoC控制反转
一般地,针对Service层和Dao层的联动,会这样写:
// Service层 public class MyService { private BookDao bookDao = new BookDaoImpl(); // 与具体类绑定,需要更换时要改动类的源码 public void save() { bookDao.save(); } } // Dao层 public interface BookDao { void save(); } public class BookDaoImpl { void save() { System.out.println("Save 1;"); } } public class BookDaoImpl2 { void save() { System.out.println("Save 2;"); } }
此时可以将
bookDao
对象的创建权转移到外部,即控制反转(Invertion of Control, IoC)。 -
DI依赖注入
在同一个IoC容器中,建立Bean之间的依赖关系,称作依赖注入。
以上两个思想的目的是为了充分解耦。解耦的思路是使用IoC容器管理Bean→在IoC容器中将有依赖关系的容器进行绑定。
-
IoC具体实现
IoC具体实现需要如下步骤。
- 导入坐标:
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>6.1.3</version> </dependency>
- 创建Spring配置文件
<?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 http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="daoImpl" class="dao.impl.DaoImpl"/> <bean id="service" class="services.Service"/> </beans>
- 初始化IoC容器
public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); Service service = (Service) context.getBean("service"); service.saveWithDao(); }
-
DI具体实现
- Service层提供
set
方法
public class Service { private Dao dao; // 相比上面的代码,不再使用new创建 public void saveWithDao() { System.out.println("Service saves:"); dao.save(); } // 这里的dao变量不再通过new的方式创建,而是通过set方法获取 public void setDao(Dao dao) { this.dao = dao; }
- 在XML的
bean
配置中设置property
属性
<bean id="service" class="services.Service"> <!--name属性表示Service类中需要注入依赖的变量--> <!--ref属性表示需要引用的具体类名--> <property name="dao" ref="daoImpl"/> </bean>
- Service层提供
6.2 Bean的配置
-
Bean的别名
使用
name
属性标明。例如:name="a1;a2,a3 a4"
,可以使用分号、逗号和空格隔开。 -
多次创建Bean
多次创建Bean时,默认只创建一个Bean对象,后续的创建仅获得首次创建的对象的引用,即单例模式。
也可以在Bean的配置中修改
property
属性,可取值为singleton
单例(默认)和prototype
原型。之所以使用单例,是因为可能一个对象要被调用多次,创建多个对象有时很浪费资源。Spring默认所管理的对象为可复用对象。
-
使用静态工厂创建Bean对象
这个方法很少用。示例:
静态工厂:
public class StaticDaoFactory { // 这里是静态方法 public static Dao getDao() { return new DaoImpl2(); } }
配置文件:
<bean id="daoImpl" class="dao.StaticDaoFactory" factory-method="getDao"/>
-
使用实例工厂创建Bean对象
首先生成Bean对象工厂的Bean实例。
示例:
public class InstantiatedDaoFactory { public Dao getDao() { System.out.println("Instantiated factory started."); return new DaoImpl(); } }
对应的配置文件如下:
<bean id="daoFac" class="dao.InstantiatedDaoFactory"/> <bean id="daoImpl" factory-bean="daoFac" factory-method="getDao"/>
-
使用
FactoryBean
创建Bean对象上述实例工厂罚创建对象时,每个对象都要创建对应的工厂,且该工厂用于创建目标对象后及被废弃,浪费资源。
为此,Spring实现了
FactoryBean<T>
接口,用于实例工厂的编写。示例:
public class DaoFactoryBean implements FactoryBean<Dao> { @Override public Dao getObject() throws Exception { return new DaoImpl(); } @Override public Class<?> getObjectType() { return Dao.class; } }
配置文件:
<bean id="daoImpl" class="dao.DaoFactoryBean"/>
注意:在该
FactoryBean
中,有一个默认实现的方法isSingleton
,通过该方法的返回值(布尔值)来确定是否为单例对象。 -
Bean的生命周期
可以给Bean定义初始化方法和销毁方法:
public interface Dao { void save(); default void init() { System.out.println("Init..."); } default void destroy() { System.out.println("Destroy..."); } }
XML配置如下:
<bean id="daoImpl" class="dao.impl.DaoImpl" init-method="init" destroy-method="destroy"/>
运行程序后,初始化方法会被调用,但程序退出时销毁方法不会被调用。若要调用销毁方法,需要注册关闭钩子:
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); context.registerShutdownHook(); // 注册关闭钩子
也可以实现
InitializingBean
、DisposableBean
两个接口:public class Service implements InitializingBean, DisposableBean { private Dao dao; public void saveWithDao() { System.out.println("Service saves:"); dao.save(); } public void setDao(Dao dao) { this.dao = dao; } @Override public void destroy() throws Exception { System.out.println("Service destroy..."); } // 该函数在setDao函数执行后执行,是初始化工作的一部分 @Override public void afterPropertiesSet() throws Exception { System.out.println("Service init..."); } }
综上,Bean的生命周期流程如下:
-
创建对象
-
执行构造方法
-
执行属性注入
-
执行Bean初始化方法
-
执行业务操作
-
执行Bean销毁方法
-
6.3 依赖注入的相关方法
-
setter
注入首先创建一个含
setter
的Bean对象:public class Service implements InitializingBean, DisposableBean { private Dao dao; // 引用类型依赖 private int serviceVal; // 基本类型依赖 private String serviceStr; // 视同基本类型依赖 public void saveWithDao() { System.out.println("Service value: " + serviceVal); System.out.println("Service string: " + serviceStr); System.out.println("Service saves:"); dao.save(); } public void setDao(Dao dao) { this.dao = dao; } public void setServiceVal(int serviceVal) { this.serviceVal = serviceVal; } public void setServiceStr(String serviceStr) { this.serviceStr = serviceStr; } @Override public void destroy() throws Exception { System.out.println("Service destroy..."); } @Override public void afterPropertiesSet() throws Exception { System.out.println("Service init..."); } }
在配置文件中的配置:
<bean id="service" class="services.Service"> <!--引用类型用ref,基本类型用value--> <property name="dao" ref="daoImpl"/> <property name="serviceVal" value="250"/> <property name="serviceStr" value="ngmhhy"/> </bean>
-
构造器注入
跟
setter
注入类似,但setter
换成了构造器。public class Service implements InitializingBean, DisposableBean { private Dao dao; private int serviceVal; private String serviceStr; // 这里将setter换成了构造器 public Service(Dao dao, int serviceVal, String serviceStr) { this.dao = dao; this.serviceVal = serviceVal; this.serviceStr = serviceStr; } public void saveWithDao() { System.out.println("Service value: " + serviceVal); System.out.println("Service string: " + serviceStr); System.out.println("Service saves:"); dao.save(); } @Override public void destroy() throws Exception { System.out.println("Service destroy..."); } @Override public void afterPropertiesSet() throws Exception { System.out.println("Service init..."); } }
配置文件内容如下:
<bean id="service" class="services.Service"> <constructor-arg name="dao" ref="daoImpl"/> <constructor-arg name="serviceVal" value="250"/> <constructor-arg name="serviceStr" value="ngmhhy"/> </bean>
也可以这样写,以降低耦合度:
<bean id="service" class="services.Service"> <constructor-arg type="dao.Dao" ref="daoImpl"/> <constructor-arg type="int" value="250"/> <constructor-arg type="java.lang.String" value="ngmhhy"/> </bean>
-
自动装配
自动装配需要用
setter
:public class Service implements InitializingBean, DisposableBean { private Dao dao; public void setDao(Dao dao) { this.dao = dao; } public void saveWithDao() { System.out.println("Service saves:"); dao.save(); } @Override public void destroy() throws Exception { System.out.println("Service destroy..."); } @Override public void afterPropertiesSet() throws Exception { System.out.println("Service init..."); } }
配置文件内容如下:
<bean id="service" class="services.Service" autowire="byType"/>
autowire
属性有以下取值:byName
、byType
、constructor
、default
和no
。一般用得较多的是byType
。
6.4 注解开发
-
使用注解代替
bean
标签首先使用
@Component
注解:@Component("dao") // 括号及字符串可以省略,默认为空 public class DaoImpl implements Dao { @Override public void save() { System.out.println("Save 1"); } }
随后,在配置文件中注明组件扫描路径:
<context:component-scan base-package="dao"/>
为了便于区分,Spring提供了3个基于@Component
的衍生注解:@Controller
表现层、@Service
业务层和@Repository
数据层。
-
纯注解开发
纯注解开发的思路是:使用类和注解替换掉applicationConfig.xml中的信息。
创建配置类代替配置文件:
@Configuration @ComponentScan({"dao", "services"}) public class SpringConfig { }
创建容器时使用注解配置容器:
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
-
Bean的作用范围和生命周期
对于IoC容器管理Bean时,是创建1个Bean还是多个Bean,通过
@Scope
注解完成。取值有2个:@Scope("prototype")
和@Scope("singleton")
。关于Bean的生命周期,可以用2个注解进行管理:
@PostConstruct
初始化和@PreDestroy
销毁。 -
注解开发的依赖注入
注解开发的依赖注入是自动装配,且不用编写
setter
方法。使用
@Autowired
进行装配。默认按类型装配,如有多个同类型对象时,按所装配的变量名进行装配。也可以使用@Qualifier("name")
按名称装配。关于简单类型,可以使用
@Value("val")
进行简单注入。加载properties文件的方法是,在
@Configuration
注解修饰的类中使用@PropertySource("xx.property")
加载文件。
6.5 纯注解开发管理第三方包
配置类的书写:
// 这里不用加@Configuration
public class DBConfig {
@Value("${driver}") // 通过properties进行依赖注入
private String driverName;
@Value("${jdbcUrl}")
private String url;
@Value("${username}")
private String username;
@Value("${password}")
private String password;
// 数据库连接池配置
@Bean // 表示当前函数返回值是一个Bean
public DataSource dataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
// 显式调用setter配置
druidDataSource.setDriverClassName(driverName);
druidDataSource.setUrl(url);
druidDataSource.setUsername(username);
druidDataSource.setPassword(password);
return druidDataSource;
}
}
Spring配置书写:
@Configuration
@ComponentScan({"dao", "services"})
@PropertySource("classpath:db.properties")
@Import(DBConfig.class) // 导入DBConfig,也可以使用包扫描注解扫描DBConfig所在目录,但在import时不用加@Configuration注解
public class SpringConfig {
}
当@Configuration
所修饰的类中的函数需要引用Bean时,在参数中传入,此时会进行自动装配:
@Bean
public DataSource dataSource(Dao dao) {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(driverName);
druidDataSource.setUrl(url);
druidDataSource.setUsername(username);
druidDataSource.setPassword(password);
dao.save(); // 自动装配dao并且引用其方法
return druidDataSource;
}
6.6 Spring整合MyBatis
-
依赖管理
Spring整合MyBatis需要额外导入2个包:
spring-jdbc
和mybatis-spring
。<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>6.1.3</version> </dependency> <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>3.0.3</version> </dependency>
-
配置信息
在这里使用Druid作为MyBatis的数据源。
数据源单独配置:
public class DataSourceConfig { @Value("${jdbc.driver}") private String driverName; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; // 数据库连接池配置 @Bean public DataSource dataSource() { DruidDataSource druidDataSource = new DruidDataSource(); druidDataSource.setDriverClassName(driverName); druidDataSource.setUrl(url); druidDataSource.setUsername(username); druidDataSource.setPassword(password); return druidDataSource; } }
再配置MyBatis:
public class MyBatisConfig { @Bean public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSrc) { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setTypeAliasesPackage("db"); sqlSessionFactoryBean.setDataSource(dataSrc); return sqlSessionFactoryBean; } // 注意:该类的配置会使@Value注入失效,应与数据源配置类分开书写 @Bean public MapperScannerConfigurer mapperScannerConfigurer() { MapperScannerConfigurer msc = new MapperScannerConfigurer(); msc.setBasePackage("DB.dao"); return msc; } }
注意:创建
MapperScannerConfigurer
类的方法会使@Value
注入值为null
,具体原因可以参考这里。
6.7 Spring整合JUnit4
测试程序如下:
@RunWith(SpringJUnit4ClassRunner.class) // 设定类运行器
@ContextConfiguration(classes = SpringConfig.class) // Spring环境配置
public class DBServiceTest {
@Autowired
private DBService dbService;
@Test
public void testSelectGoods() {
System.out.println(dbService.selectAllGoods());
}
}
6.8 AOP详解
AOP(Aspect Oriented Programming,面向切面编程)可以在不惊动原始程序设计的基础上进行功能增强。语法可以参考Python的装饰器。
-
概念
连接点:程序执行中的任意位置。
切入点:匹配连接的式子。
通知:在切入点执行的操作。
切面:通知与切入点的对应关系。
-
操作
导入相关依赖:
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.21</version> </dependency>
配置切面:
@Component // 让Spring IoC容器导入该类 @Aspect // 标明这是一个切面 public class MyAdvice { @Pointcut("execution(void services.DBService.aopTest())") // 配置切入点 private void m() { } @Before("m()") // 配置通知 public void method() { System.out.println(System.currentTimeMillis()); } }
最后,在
SpringConfig
配置类中加入注解@EnableAspectJAutoProxy
,以识别@Aspect
等注解。 -
AOP环绕
环绕型AOP需要注意,将原有异常抛出或处理,及时返回原有值。
@Pointcut("execution(* services.DBService.aopTest())") // 表达式要匹配,否则无法选定切入点 private void m() { } @Around("m()") public Integer method(ProceedingJoinPoint pjp) throws Throwable { System.out.println(System.currentTimeMillis()); Integer proceeded = (Integer) pjp.proceed(); // 配置切入点 System.out.println("Finish..."); return proceeded; }
6.9 事务管理
事务是一组能同时成功或同时失败的操作。
-
事务管理操作
首先要对纳入事务管理的方法使用
@Transactional
注解:@Transactional public void transact(short moneyFrom, short moneyTo, BigDecimal transMoney) { acctDao.minusMoney(moneyFrom, transMoney); acctDao.addMoney(moneyTo, transMoney); }
然后注入事务管理器:
// JDBC事务配置 @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); }
最后在Spring配置中开启事务处理:
@EnableTransactionManagement
。 -
事务角色
对于MyBatis的2个操作:
acctDao.minusMoney(moneyFrom, transMoney)
和acctDao.addMoney(moneyTo, transMoney)
,分别为2个毫不相干的事务。因此,出现异常时并不会导致互相回滚。在Spring中开启事务后,会使2个数据层事务加入Spring事务。此时数据层充当事务协调员的角色,Spring充当事务管理员的角色。
由于MyBatis和
DataSourceTransactionManager
使用的是同一个数据源,因此Spring才能将数据层纳入事务管理范畴。 -
事务属性和配置
在注解
@Transactional(property1 = val1, ...)
中配置的为属性。属性的配置见下:
属性 作用 示例 readOnly
只读事务 readOnly = true
为只读事务timeout
事务超时 timeout = -1
永不超时rollbackFor
事务回滚异常(主要用于受检异常) rollbackFor = IOException.class
noRollbackFor
事务不回滚的异常 noRollbackFor = NullPointerException.class
propagation
事务传播行为 propagation = propagation.REQUIRES_NEW
第7章 Spring MVC
Spring MVC是Spring的一部分,是Web开发技术,用于简化Servlet开发。
7.1 Spring MVC配置
首先导入Servlet和Spring MVC的依赖:
<!-- https://mvnrepository.com/artifact/jakarta.servlet/jakarta.servlet-api -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.1.0-M1</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.1.3</version>
</dependency>
接下来导入编写控制器:
@Controller
public class UserController {
@GetMapping("/save")
@ResponseBody // 将返回值作为响应负载,可以使用@RestController简化
public String save() {
System.out.println("User save...");
return "{\"module\": \"Spring MVC\"}";
}
}
然后设置Spring MVC环境:
@Configuration
@ComponentScan("controllers")
public class SpringMVCConfig {
}
最后配置Tomcat启动类:
public class TomcatInitConfig extends AbstractDispatcherServletInitializer {
// 加载Spring MVC容器配置
@Override
protected WebApplicationContext createServletApplicationContext() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(SpringMVCConfig.class);
return context;
}
// 设置归属Spring MVC处理的请求
@Override
protected String[] getServletMappings() {
return new String[]{"/"}; // 所有请求归属Spring MVC处理
}
// 加载Spring容器
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
}
或者:
public class TomcatInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
// Spring容器配置
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{SpringConfig.class};
}
// Spring MVC容器配置
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{SpringMVCConfig.class};
}
// Spring MVC处理的请求
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
-
Spring整合Spring MVC的Bean管理
Bean管理的常用方式是,Spring MVC只管理Controller,Spring管理Service和Dao。
7.2 Controller编写的注意事项
-
配置请求路径前缀
有时,需要区分不同的模块,可以在Controller类的前面加上
@RequestMapping
注解:@Controller @RequestMapping("/user") // 设置路径前缀 public class UserController { @Autowired private UserDao userDao; @GetMapping("/show") @ResponseBody // 将返回值作为响应负载 public String show() { System.out.println("User save..."); return "{\"module\": \""+ userDao.getUser() +"\"}"; } }
此时的路径就变成了
/user/show
。 -
各种请求的接收
根据请求信息格式的不同,可以如此处理请求:
@PostMapping(value = "/update", produces = "application/json;charset=UTF-8") public String update(@RequestParam("info") String info) { return "{\"info\":\"" + info + "\"}"; } @GetMapping("print_user") public String printUser(User user) { return user.toString(); } // 多个参数使用数组接收 @PostMapping("/multiple_vars1") public String handleMul1(@RequestParam("likes") String[] likes) { return Arrays.toString(likes); } // 多个参数使用数组接收 @PostMapping("/multiple_vars2") public String handleMul2(@RequestParam("likes") List<String> likes) { return likes.toString(); } // 处理JSON格式 @PostMapping("/handle_json") public String handleJSON(@RequestBody List<String> jsonString) { return jsonString.toString(); } // 处理日期格式 @GetMapping("/handle_date") public String handleDate(@RequestParam("date") @DateTimeFormat(pattern = "yy-MM-dd HH:mm:ss") Date date) { return date.toString(); }
解析JSON时,要导入依赖:
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.16.1</version> </dependency>
并在Spring MVC配置中设置
@EnableWebMvc
注解。 -
响应处理
使用
@ResponseBody
注解时,会将控制器方法的返回值直接作为响应负载返回。若控制器返回值为一个pojo对象,会将其转换为一个JSON字符串。注:若一个控制器类中所有方法均用
@ResponseBody
修饰时,可以直接对控制器使用@RestController
注解代替@Controller
注解,以省略对每一个方法的@ResponseBody
注解。
7.3 REST风格
REST(Representational State Transfer,表现形式状态转换)通过路径和请求类型判断操作。
-
RESTful开发
参数写在URL上时,可以这样解析URL:
@GetMapping("/url_param/{varParam}") public String getParamFromURL(@PathVariable("varParam") Integer varParam) { return varParam.toString(); }
通常情况下使用
@RestController
开发。
第8章 Spring Boot
8.1 SpringBoot基础知识
8.1.1 SpringBoot概念
SpringBoot是一种快速使用Spring的方式。
Spring的官网在这里。
SpringBoot是为了改善Spring的一些缺点:
-
配置繁琐:Spring使用大量XML或注解,增大了开发工作量。
-
依赖繁琐:搭建环境时,需要导入大量坐标,有时还会出现版本兼容性的问题。
SpringBoot功能:
-
自动配置:在应用程序启动时自动配置好Spring的相关配置。
-
起步依赖:定义了对其它库的传递依赖,或者说将具备某种功能的坐标打包在一起。
-
辅助功能:嵌入式服务器、安全、指标等。
总之,SpringBoot不是对Spring功能的增强,而是提供了一种快速使用Spring的方式。
8.1.2 SpringBoot开发流程
SpringBoot实现一个简单操作的流程:创建Maven项目、导入SpringBoot起步依赖、定义Controller、编写引导类、启动测试。
首先引入如下依赖:
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-parent -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.1</version>
<type>pom</type>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.1</version>
</dependency>
随后创建Controller:
@RestController
public class HelloController {
@RequestMapping("/hello")
public String Hello() {
return "Hello, Spring Boot!";
}
}
随后创建引导类:
// 引导类:Spring Boot的入口
@SpringBootApplication
public class HelloApplication {
public static void main(String[] args) {
SpringApplication.run(HelloApplication.class, args);
}
}
运行方法,根据下面的日志找到启动端口8080,浏览器输入localhost:8080/hello
,即可看到页面。
注意:SpringBoot使用jar包而不是war包。
8.1.3 Spring Boot配置
Spring Boot基于约定,故很多设置都有默认值。配置支持properties、yml和yaml文件。
yml语法:
server:
port: 80
address: 127.0.0.1
person: {name: Zhangsan}
address:
- Beijing
- Shanghai
group: [root, Administrators]
msg1: 'Hello \n world' # 单引号忽略转义字符
msg2: "Hello \n world" # 双引号识别转义字符
有以下特点:
-
大小写敏感
-
数据前面有空格作为分隔符
-
使用缩进表示层级关系,切锁紧不能使用Tab键
-
#
表示单行注释
-
Spring Boot获取配置
除了
@Value
注解获取配置以外,还有Environment
对象获取数据和@ConfigurationProperties
注解方法。Environment
对象获取数据,通过调用getProperties()
方法获取。@ConfigurationProperties
注解直接注入pojo中。 -
profile
功能profile
用于切换生产、开发、测试环境。在
application
的配置文件中写入spring.profiles.active
配置。 -
配置加载顺序(优先级)
Spring Boot会按以下优先级加载配置文件:
-
file:./config
,项目的config
目录下。 -
file:./
,项目的根目录下。 -
classpath:/config
,资源的config
目录下。 -
classpath:/
,资源根目录下。
-
8.1.4 Spring Boot整合JUnit
Spring Boot整合JUnit的框架如下:
@SpringBootTest
public class TestServiceTest {
@Autowired
private TestService testService;
@Test
public void testTestService() {
testService.serve();
}
}
8.2 Spring Boot进阶
8.2.1 Spring Boot自动配置
-
Condition
接口用于选择性地加载
Bean
。示例:需求是若导入Spring Data Redis才加载Bean。
代码:
@Conditional(LoadRedisCondition.class) // 条件注解,满足条件才会创建该Bean @Bean public User getUser() { return new User(1, "Zhang San"); }
Condition
实现类:public class LoadRedisCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { // 未获取到该类加载,则返回假 try { Class<?> rt = Class.forName("org.springframework.data.redis.core.RedisTemplate"); } catch (ClassNotFoundException e) { return false; } return true; } }
调用语句:
ConfigurableApplicationContext context = SpringApplication.run(SpringBootTrainApplication.class, args); System.out.println(context.getBean(User.class));
-
切换服务器应用
Spring Boot默认使用Tomcat,但可以切换成Jetty、Netty等多种服务器。
切换方法:在
spring-boot-starter-web
依赖中排除spring-boot-starter-tomcat
,然后引入spring-boot-starter-jetty
或其它服务器的Spring Boot起步依赖。示例:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jetty</artifactId> </dependency>
第9章 MyBatis Plus
MyBatis Plus用于简化MyBatis开发。
9.1 MyBatis Plus入门
首先引入依赖,注意目前最高版本的Spring Boot存在版本冲突的Bug,需要排除原有包的mybatis-spring
依赖并导入新包:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
<exclusions>
<!-- 排除依赖版本冲突,这是针对Spring Boot 3.2.3 -->
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
针对以下数据库:
create database mybatis_plus_train default charset utf8mb4;
use mybatis_plus_train;
create table user (
id int unsigned not null unique auto_increment,
username varchar(20) not null,
email varchar(50) not null unique,
primary key (id),
constraint email_check check (email regexp '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}') -- 使用正则表达式进行检查
);
随后编写对应的POJO和Mapper:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User { // 注意,pojo类名要与数据库表名一致,成员变量名与表的列名对应(驼峰命名和下划线命名)
private int id;
private String username;
private String email;
}
@Mapper
public interface UserDao extends BaseMapper<User> {
}
只需继承BaseMapper<T>
,无需编写任何方法,即可获取基本的增删改查操作。操作详见这里。
9.2 MyBatis Plus查询
-
分页查询
操作数据库时,分页查询的逻辑是,查询出符合条件的全部内容后,对查询结果进行拦截和过滤。
分页查询涉及到
Page
类和IPage
接口。使用方法:
首先添加分页拦截器:
@Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 定义MP拦截器 interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 添加具体的拦截器 return interceptor; } }
随后开始查询:
@Test public void testSelectLimit() { IPage<Student> page = new Page<>(2L, 8); IPage<Student> selectedPage = studentDao.selectPage(page, null); System.out.println("当前页码值:" + selectedPage.getCurrent()); System.out.println("每页大小:" + selectedPage.getSize()); System.out.println("总共页数:" + selectedPage.getPages()); System.out.println("总共数据条数:" + selectedPage.getTotal()); System.out.println("数据:" + selectedPage.getRecords()); }
-
条件查询
MyBatis Plus将复杂的SQL语句进行了封装,使用编程完成查询条件的组合。
条件查询通过
QueryWrapper<T>
实现。示例:
@Test public void testQueryWrapper() { QueryWrapper<Student> qw = new QueryWrapper<>(); qw.lambda().lt(Student::getStuId, "23011060"); // 这里使用了lambda格式的条件查询,并使用了方法引用 List<Student> list = studentDao.selectList(qw); System.out.println(list); }
或者简化为:
@Test public void testQueryWrapper() { LambdaQueryWrapper<Student> qw = new LambdaQueryWrapper<>(); qw.lt(Student::getStuId, "23011060"); // 直接使用lambda格式 List<Student> list = studentDao.selectList(qw); System.out.println(list); }
多条件并列查询:
qw.gt(Student::getStuId, "23011020").lt(Student::getStuId, "23011040"); // 与条件查询 qw.lt(Student::getStuId, "23011030").or().gt(Student::getStuId, "23011050"); // 或条件查询
还有一种写法,即
qw.gt(true, Student::getStuId, "23011050");
,前面的boolean
值表示,若该值为真,则连接所设定的条件。 -
投影查询
即查询部分列。
用Lambda格式:
@Test public void testQueryCast() { LambdaQueryWrapper<Student> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.select(Student::getName, Student::getPasswordHash); // 传pojo的getter方法 queryWrapper.lt(Student::getStuId, "23011030").or().gt(Student::getStuId, "23011050"); List<Student> students = studentDao.selectList(queryWrapper); System.out.println(students); }
不用Lambda格式:
@Test public void testQueryCastWithoutLambda() { QueryWrapper<Student> queryWrapper = new QueryWrapper<>(); queryWrapper.select("name", "password_hash"); queryWrapper.gt("stu_id", "23011020").lt("stu_id", "23011035"); // 传数据库表的列名 List<Student> students = studentDao.selectList(queryWrapper); System.out.println(students); }
-
分组查询:
@Test public void testQueryCountWithoutLambda() { QueryWrapper<Student> queryWrapper = new QueryWrapper<>(); queryWrapper.select("count(*) as count", "class_belong_to"); queryWrapper.groupBy("class_belong_to"); List<Map<String, Object>> selectedMaps = studentDao.selectMaps(queryWrapper); // 由于Student类没有count属性,使用Map承接 System.out.println(selectedMaps); }
-
模糊查询
有
like
、likeLeft
、notLike
等方法进行模糊查询。示例:
@Test public void testQueryLike() { QueryWrapper<Student> queryWrapper = new QueryWrapper<>(); queryWrapper.like("stu_id", "65"); Student student = studentDao.selectOne(queryWrapper); System.out.println(student); }
9.3 MyBatis Plus高级
MyBatis Plus有一些深入的设计。
-
表名或字段名冲突问题
使用
@TableField
注解或@TableName
注解进行处理。可以解决的场景为:数据库字段与pojo类成员变量名不一致问题
@TableField("xxx")
;类名在数据库表中不存在的问题@TableField(exist = false)
;某些字段不参与查询@TableField(select = false)
;表名与pojo类名不一致问题@TableName("xxx")
。表名前缀可以通过在
application.yml
中配置全局设置。mybatis-plus: global-config: db-config: table-prefix: tbl_
-
ID生成策略
针对不同的业务,有不同的业务生成策略。
生成策略 解释 AUTO
使用数据库自增策略 NONE
不设置ID生成策略 INPUT
手工输入ID ASSIGN_ID
雪花算法生成ID ASSIGN_UUID
UUID生成算法生成ID 也可以定义全局配置:
mybatis-plus: global-config: db-config: id-type: auto
-
多数据操作
多数据查询和多数据删除,可以用
selectBatchIds()
或deleteBatchIds()
操作。示例:
@Test public void multipleDataOperation() { ArrayList<Integer> idList = new ArrayList<>(); idList.add(23011011); idList.add(23011014); idList.add(23011037); List<Student> students = studentDao.selectBatchIds(idList); System.out.println(students); }
-
逻辑删除
逻辑删除并不会将原有的信息从数据库中删除,而是添加一个标记字段,标记已删除的信息。
首先在表中设立字段,用于标记删除状态:
alter table add deleted tinyint(1) not null default 0;
随后在pojo类的相关字段上添加
@TableLogic
注解:@TableLogic(value = "0", delval = "1") private Byte deleted;
当调用
deleteById()
方法时,实际执行的SQL语句变为:update user set delete := 1 where id = xxx and deleted = 0;
查询数据库字段时,实际执行的SQL语句变为:
select id, `name`, xxx, yyy from user where delete = 0;
同样可以进行全局配置:
mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0
9.4 乐观锁和代码生成器
-
乐观锁
乐观锁用于解决并发量不那么高时的场景,用于提高效率,机制可以参考这里。
首先添加拦截器:
@Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 定义MP拦截器 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 添加乐观锁拦截器 return interceptor; } }
随后添加Version成员变量,并在数据库中添加对应字段(略):
@Version private Integer version;
最后通过pojo操作数据库:
Student student = studentDao.selectById(23011021); // 查询原有数据,获取version值,没有这一步无法使用乐观锁 student.setXXX("xxx"); ... studentDao.updateById(student);
-
模板生成器
导入坐标:
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-generator --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.5</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.velocity/velocity-engine-core --> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.3</version> </dependency>
详见这里。其余略。
第10章 WebSocket
严格意义来说,WebSocket并不是Java Web独有的内容,而是一种应用层协议,可以在多种语言上实现。
从Tomcat 7开始支持WebSocket,并实现了WebSocket规范。
WebSocket应用由一系列Endpoint
组成,可以通过继承javax.websocket.Endpoint
类或使用@ServerEndpoint
有关注解来编写Endpoint。
Endpoint
定义了生命周期的相关方法:
方法 | 注解 | 描述 |
---|---|---|
onOpen() | @OnOpen | 开启一个新的会话时调用 |
onClose() | @OnClose | 会话关闭时调用 |
onError() | @OnError | 连接异常时调用 |
接受客户端数据可以通过添加MessageHandler
接收消息,或者通过@OnMessage
注解指定接收消息的方法。
一个Endpoint
的结构如下:
@ServerEndpoint(value = "/chat", configurator = GetHttpSessionConfig.class) // 声明访问路径
@Component // 交由Spring IoC容器管理
public class ChatEndpoint {
// 由于每个连接均会创建一个Endpoint,该变量应声明为整个类共有的变量,故使用static关键字;Concurrent表明线程安全
private static final Map<String, Session> onlineUsers = new ConcurrentHashMap<>();
private HttpSession httpSession;
@OnOpen // 开启连接时
public void onOpen(Session session, EndpointConfig config) {
// 1. 保存Session
httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
String user = (String) httpSession.getAttribute("user");
onlineUsers.put(user, session);
// 2. 广播上线用户,即将上线的用户推送给所有用户
Set<String> friends = onlineUsers.keySet();
String sysMessage = MessageUtils.getMessage(true, null, friends);
broadcastAllUsers(sysMessage);
}
private void broadcastAllUsers(String message) {
Set<Map.Entry<String, Session>> entries = onlineUsers.entrySet();
for (Map.Entry<String, Session> entry : entries) {
Session wsSession = entry.getValue();
try {
wsSession.getBasicRemote().sendText(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@OnMessage // 收到来自浏览器的数据时
public void onMessage(String message) {
// 推送消息给指定用户
Message messagePojo = JSON.parseObject(message, Message.class);
String toName = messagePojo.getToName();
// 获取接收方的Session对象
Session toSession = onlineUsers.get(toName);
String user = (String) httpSession.getAttribute("user");
String sendMessage = MessageUtils.getMessage(false, user, messagePojo.getMessage());
try {
toSession.getBasicRemote().sendText(sendMessage);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@OnClose
public void onClose(Session session) {
// 1. 删除当前用户的Session对象
String user = (String) httpSession.getAttribute("user");
onlineUsers.remove(user);
// 2. 通知其他用户下线消息
Set<String> friends = onlineUsers.keySet();
String sysMessage = MessageUtils.getMessage(true, null, friends);
broadcastAllUsers(sysMessage);
}
}
第11章 分布式服务与Dubbo
Dubbo是一个Java RPC框架。
对于小型项目,一般把Service层与Controller层放在一个项目中,但是分布式项目会将Service层独立出来作为一个独立的项目,二者之间使用远程过程调用(Remote Procedure Call,RPC)进行联系。
11.1 互联网服务架构
互联网架构从原来的单机服务开始,发展到分布式,主要有以下几种架构:
-
RPC架构:通过网络从远程计算机请求服务,而不需要了解底层的网络协议。代表技术有Thrift、Hessian等。特点是应用直接调用服务,服务之间是隔离的。缺点是服务过多时,管理成本高昂。
-
SOA(Service Oriented Architecture,面向服务)架构:通过一个ESB(Enterprise Service Bus,企业服务总线)作为服务中介提供服务与服务之间的交互。
-
微服务架构:将一个大型项目分解为若干个微服务。这些微服务可以被独立部署,各个微服务之间是松耦合的。每个微服务仅关注于完成一件事并很好地完成该任务。微服务就是一个轻量级的服务治理方案。对比SOA架构,使用注册中心代替ESB服务总线。注册中心相比ESB更加轻量。代表技术有Spring Cloud、Dubbo等。
11.2 RPC协议
远程过程调用(Remote Procedure Call,RPC)协议,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。
RPC采用客户机——服务器模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,客户机调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息到达为止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息,最后,客户端调用进程接收答复信息,获得进程结果,然后调用执行继续进行。
11.3 Spring RMI
Spring RMI是一个较为传统的RPC框架。
首先构建父工程,并引入较老版本的Spring Boot起步依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<!-- 较新版本的spring-context所包含的rmi库不全 -->
<version>2.2.1.RELEASE</version>
<relativePath/>
</parent>
除此之外,还要配置父工程的打包方式:
<properties>
<maven.compiler.source>21</maven.compiler.source>
<!-- 较老版本的Spring Boot不支持过高版本的JDK -->
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<packaging>pom</packaging>
构建3个子工程:RMI-API
、RMI-Server
、RMI-Client
。在RMI-API
中构建pojo和服务接口,由RMI-Server
实现该接口,RMI-Client
调用该接口在RMI-Server
中的实现。
RMI-API
的pojo:
@Data // 这几个注解用了lombok,这里不再赘述
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable { // 由于需要在网络中传输该pojo,需要将其序列化
private int id;
private String username;
}
服务接口:
public interface UserService {
User getUserById(int uid) throws RemoteException; // 由于使用远程调用,需要显式抛出异常RemoteException
}
服务端RMI-Server
提供服务接口的具体实现。Maven配置如下:
<!-- 所有的子模块都要注明父项目,包括RMI-API、RMI-Client -->
<parent>
<groupId>org.javatrain</groupId>
<artifactId>RMI</artifactId>
<version>1.0</version>
</parent>
在该项目中,完成UserService
的具体实现:
@Service // 注册为服务类
public class UserServiceImpl implements UserService {
@Override
public User getUserById(int uid) throws RemoteException {
System.out.println("服务端收到请求,请求参数为" + uid);
return new User(uid, "Kunkun");
}
}
同时配置RMI服务端:
@Configuration
public class RmiServerConfig {
@Bean
public RmiServiceExporter rmiServiceExporter(UserService userService) {
RmiServiceExporter exporter = new RmiServiceExporter();
exporter.setRegistryPort(2002); // RMI连接端口
exporter.setServiceName("userService"); // RMI服务名
exporter.setService(userService); // RMI服务所调用的类
exporter.setServiceInterface(UserService.class); // RMI服务类所实现的接口
return exporter;
}
}
客户端RMI-Client
通过RMI远程调用位于服务端的服务类。需要实现RMI客户端配置:
@Configuration
public class RmiClientConfig {
@Bean(name = "userService") // 给定名字,以便使用自动注入
public RmiProxyFactoryBean rmiProxyFactoryBean() {
RmiProxyFactoryBean rmiProxy = new RmiProxyFactoryBean();
rmiProxy.setServiceUrl("rmi://localhost:2002/userService"); // 该URL中,访问路径为RMI服务名,端口即为服务端口,协议为RMI协议
rmiProxy.setServiceInterface(UserService.class);
return rmiProxy;
}
}
随后真正调用该类:
@RestController
public class UserController {
@Resource // 这里也可以使用@Autowired注解
private UserService userService; // 此时自动注入的是RMI的动态代理,像是完全没用RMI时的写法
@RequestMapping("/user")
public User getUser() throws RemoteException {
return userService.getUserById(1);
}
}
11.4 Dubbo使用
Spring RMI的服务发布和订阅需要手动配置,当服务较多时相当麻烦。
Dubbo提供了六大核心能力:面向接口代理的高性能RPC调用、智能容错和负载均衡、服务自动注册和发现、高度可扩展能力、运行期流量调度、可视化的服务治理与运维。
与Spring RMI类似,开发模块都需要一个API、一个Server(Provider)和一个Client(Consumer)。
11.4.1 不使用ZooKeeper时配置Dubbo
此时使用Multicast作为注册中心。
父模块进行如下配置:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.3</version>
<relativePath/>
</parent>
<packaging>pom</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<dubbo.version>3.2.11</dubbo.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
API模块与RMI-API
配置一致。
Provider模块配置主要是在YAML文件中,以及注解配置:
server:
port: 8001
# Dubbo context
dubbo:
application:
name: provider # Application name
registry:
address: multicast://224.5.6.7:1234 # Broadcast method
protocol: # Registry protocol
name: dubbo
port: 20880
scan: # Service packages
base-packages: org.javatrain.dubbotrain.service
在服务实现类上使用注解:
@DubboService(version = "1.0") // 设置版本1.0
public class UserServiceImpl implements UserService {
@Override
public User getUserById(int id) {
System.out.println("服务提供方收到请求,参数为" + id);
return new User(id, "Kun Dubbo");
}
}
客户端也是同理:
server:
port: 80
dubbo:
application:
name: dubbo-consumer
registry:
address: multicast://224.5.6.7:1234
控制器类配置如下:
@RestController
public class UserController {
@Reference(version = "1.0", parameters = {"unicast", "false"}) // unicast=false,则广播给订阅者
private UserService userService;
@GetMapping("/user/{uid}")
public User getUserById(@PathVariable int uid) {
return userService.getUserById(uid);
}
}
11.4.2 使用ZooKeeper集群配置Dubbo
使用Multicast作为注册中心不适用于较大的集群。此时应该使用ZooKeeper。
在父模块中引入以下依赖:
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<dubbo.version>3.2.11</dubbo.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-dependencies-zookeeper</artifactId>
<version>${dubbo.version}</version>
<exclusions>
<!-- 这里的slf4j-reload4j与dubbo-spring-boot-starter中含有的slf4j冲突 -->
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-reload4j</artifactId>
</exclusion>
<!-- 这里原有的zookeeper模块过于老旧,需要排除 -->
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
<type>pom</type>
</dependency>
<!-- 更换匹配版本的zookeeper模块 -->
<!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.8.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
相比使用multicast作为注册中心,仅需改动YAML文件的dubbo.registry.address
项,值为zookeeper://master:2181?backup=slave:2181,slave2:2181
。