Java Web学习笔记

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查询表中数据

查询表中数据的操作流程如下。

  1. 建表、添加数据。

  2. 创建模块,导入坐标。

  3. 编写MyBatis配置文件。

  4. 编写SQL映射文件。

  5. 编码。

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代理开发的流程如下。

  1. 定义与SQL映射文件同名的Mapper接口,并将Mapper接口和SQL映射文件放置在同一个目录下。

注:在src文件夹下创建一个xxx.yyy目录,代码放里面;在resources文件夹下创建一个xxx/yyy目录,同名配置文件放里面。

  1. 设置SQL映射文件的namespace属性为Mapper全限定名。

即:<mapper namespace="xxx.yyy.testclass">

  1. 在Mapper接口下定义方法,方法名为SQL映射文件中SQL语句的ID,保持参数类型与返回值一致。

  2. 编码。

编码内容较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的使用方法:

    1. select标签前使用resultMap标签;

    2. 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 &lt;

    CDATA区:<![CDATA[<]]>

3.8 添加操作

  • insert操作的步骤

    1. 编写Mapper接口方法

      参数为除了id以外的所有数据。

      方法名:void insert_xxx(Brand brand);

    2. 在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>
      
    3. 执行方法

      执行方法的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 删除操作

  • 单个删除操作

    1. 在Mapper接口中编写方法:void deleteById(int id)

    2. 编写SQL映射文件:

      <delete id="deleteById">
          delete from brands where id = #{id};
      </delete>
      
  • 批量删除

    1. 编写接口方法:void deleteByIds(@Param("ids")int[] ids);

    2. 在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映射文件中设置的参数名一致。

单个参数时:

  1. POJO类:直接使用,属性名和参数占位符名称一致。

  2. Map集合:直接使用,键名和参数占位符名称一致。

  3. Collection:封装为Map集合。

    map.put("arg0", collection_set);
    map.put("collection", collection_set);
    
  4. List:

    map.put("arg0", list);
    map.put("collection", list);
    map.put("list", list);
    
  5. Array:与Collection类似。

  6. 其它类型:直接使用。

注意:需要封装的类型,尽量使用@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大域对象:

    1. page:当前页面有效。

    2. request:当前请求有效。

    3. session:当前会话有效。

    4. 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具体实现需要如下步骤。

    1. 导入坐标:
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>6.1.3</version>
    </dependency>
    
    1. 创建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>
    
    1. 初始化IoC容器
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        Service service = (Service) context.getBean("service");
        service.saveWithDao();
    }
    
  • DI具体实现

    1. 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;
    }
    
    1. 在XML的bean配置中设置property属性
    <bean id="service" class="services.Service">
        <!--name属性表示Service类中需要注入依赖的变量-->
        <!--ref属性表示需要引用的具体类名-->
        <property name="dao" ref="daoImpl"/>
    </bean>
    

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(); // 注册关闭钩子
    

    也可以实现InitializingBeanDisposableBean两个接口:

    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的生命周期流程如下:

    1. 创建对象

    2. 执行构造方法

    3. 执行属性注入

    4. 执行Bean初始化方法

    5. 执行业务操作

    6. 执行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属性有以下取值:byNamebyTypeconstructordefaultno。一般用得较多的是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-jdbcmybatis-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的一些缺点:

  1. 配置繁琐:Spring使用大量XML或注解,增大了开发工作量。

  2. 依赖繁琐:搭建环境时,需要导入大量坐标,有时还会出现版本兼容性的问题。

SpringBoot功能:

  1. 自动配置:在应用程序启动时自动配置好Spring的相关配置。

  2. 起步依赖:定义了对其它库的传递依赖,或者说将具备某种功能的坐标打包在一起。

  3. 辅助功能:嵌入式服务器、安全、指标等。

总之,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"  # 双引号识别转义字符

有以下特点:

  1. 大小写敏感

  2. 数据前面有空格作为分隔符

  3. 使用缩进表示层级关系,切锁紧不能使用Tab键

  4. #表示单行注释

  • Spring Boot获取配置

    除了@Value注解获取配置以外,还有Environment对象获取数据和@ConfigurationProperties注解方法。

    Environment对象获取数据,通过调用getProperties()方法获取。

    @ConfigurationProperties注解直接注入pojo中。

  • profile功能

    profile用于切换生产、开发、测试环境。

    application的配置文件中写入spring.profiles.active配置。

  • 配置加载顺序(优先级)

    Spring Boot会按以下优先级加载配置文件:

    1. file:./config,项目的config目录下。

    2. file:./,项目的根目录下。

    3. classpath:/config,资源的config目录下。

    4. 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);
    }
    
  • 模糊查询

    likelikeLeftnotLike等方法进行模糊查询。

    示例:

    @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_UUIDUUID生成算法生成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 互联网服务架构

互联网架构从原来的单机服务开始,发展到分布式,主要有以下几种架构:

  1. RPC架构:通过网络从远程计算机请求服务,而不需要了解底层的网络协议。代表技术有Thrift、Hessian等。特点是应用直接调用服务,服务之间是隔离的。缺点是服务过多时,管理成本高昂。

  2. SOA(Service Oriented Architecture,面向服务)架构:通过一个ESB(Enterprise Service Bus,企业服务总线)作为服务中介提供服务与服务之间的交互。

  3. 微服务架构:将一个大型项目分解为若干个微服务。这些微服务可以被独立部署,各个微服务之间是松耦合的。每个微服务仅关注于完成一件事并很好地完成该任务。微服务就是一个轻量级的服务治理方案。对比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-APIRMI-ServerRMI-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

  • 21
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值