JavaWeb学习笔记
系统学习 Java | Web开发 0 431

三层架构: 这种架构的设计有助于实现代码的模块化、可维护性和可扩展性

  • 表现层(web, Presentation Layer): 也称为视图层,在前后端分离的情况下负责处理前端请求并响应JSON数据,在前后端不分离情况下则需要响应HTML页面
  • 业务层(service, Business Layer): 也称为服务层,实现应用的业务逻辑
  • 持久层(dao[Data Access Object], Persistence Layer): 负责处理数据的持久化,即数据的存储和检索
  • 数据层: 包含所有的Value Object(pojo/vo)
    • 特点: 每个类都是数据容器,封装属性对应的值,都会提供简单的get, setter方法
    • 分类:
      • entity: 与数据库中的表一一对应的数据对象
        • Persistent Object(po): 早期会用,不仅与表一一对应,还会直接提供操作表的方式【e.g. save, update
      • Domain Object(do/domain/bo): 领域/业务对象,指在业务操作时与业务流程直接相关的对象
      • View Object(vo): 视图(请求与响应)相关的数据对象,不参与业务的流程
      • Data Transfer Object(dto): 数据传输对象,用于微服务之间的数据传输
    • 优先级: Domain Object > Persistent Object > View Object > Value Object

MyBatis

  • ORM(对象关系映射): 提供了对数据库操作的抽象,使开发者能够通过操作对象而不是直接编写SQL来进行数据库操作【Django ORM, Hibernate】
  • Persistence API(持久化API): 提供低级别的数据库操作,允许更细粒度的控制【MyBatis】

框架结构

  • MyBatis配置
    • 核心配置: MyBatisConfig.xml【包含连接参数和加载的映射文件】
    • 映射配置: xxxMapper.xml
  • SqlSessionFactory: 用来创建会话对象
    Inputstream is = Resources.getResourceAsStream("mybatis-config.xml");
    SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
    SqlSessionFactory factory = builder.build(is); 
  • SqlSession: 表示会话对象,底层封装的是Connection连接对象
    SqlSession sqlSession = sqlSessionFactory.openSession(true); // 自动事务
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class); // 动态代理
    List<User> userList = userMapper.findAllUser();
    // sqlSession.commit();
    sqlSession.close();
  • Executor: 使用SqlSession会话对象调用执行器执行sql语句
  • Mapped Statement: 返回List,String,Integer,POJO

核心配置文件

  • properties(属性): 加载外部的资源配置文件【例如保密信息】
    jdbc.driver=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3307/testdb?allowMultiQueries=true
    jdbc.username=root
    jdbc.password=1234asdw
    <properties resource="org/mybatis/example/config.properties">
      <property name="username" value="dev_user"/>
      <property name="password" value="F2Fa3!33TYyg"/>
    </properties>
    
    <dataSource type="POOLED">
      <property name="driver" value="${driver}"/>
      <property name="url" value="${url}"/>
      <property name="username" value="${username}"/>
      <property name="password" value="${password}"/>
    </dataSource>
  • settings(设置): 改变MyBatis的运行时行为
    • 开启驼峰映射: 把数据表中的带有下划线的字段,变为驼峰命名方式【例如user_name=>usernameuserName
      <settings>
          <setting name="mapUnderscoreToCamelCase" value="true" />
      </settings>
  • typeAliases(类型别名): 给类的全限定名称(包名.类名)取一个短名称
    • 自定义别名:
      <typeAliases>
        <typeAlias alias="Author" type="domain.blog.Author"/>
        <typeAlias alias="Blog" type="domain.blog.Blog"/>
        <typeAlias alias="Comment" type="domain.blog.Comment"/>
        <typeAlias alias="Post" type="domain.blog.Post"/>
        <typeAlias alias="Section" type="domain.blog.Section"/>
        <typeAlias alias="Tag" type="domain.blog.Tag"/>
      </typeAliases>
      <typeAliases>
        <package name="domain.blog"/>
      </typeAliases>
    • 内置别名: 常见内置类型的小写形式
  • typeHandlers(类型处理器): 将获取到的值以合适的方式转换成Java类型
  • objectFactory(对象工厂)
  • plugins(插件)
  • environments(环境配置):【实际使用场景下,更多的是用spring来管理数据源】
    <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>
    </environments>
    • environment(环境变量)
      • transactionManager(事务管理器): 默认使用JDBC
      • dataSource(数据源): 默认类型为POOLED
  • databaseIdProvider(数据库厂商标识)
  • mappers(映射器)
    • 使用相对于类路径的资源引用
      <mappers>
        <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
        <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
        <mapper resource="org/mybatis/builder/PostMapper.xml"/>
      </mappers>
    • 将包内的映射器接口全部注册为映射器
      <mappers>
        <package name="org.mybatis.builder"/>
      </mappers>
    • 使用映射器接口实现类的完全限定类名【需要确保xml位置正确】
      <mappers>
        <mapper class="org.mybatis.builder.AuthorMapper"/>
        <mapper class="org.mybatis.builder.BlogMapper"/>
        <mapper class="org.mybatis.builder.PostMapper"/>
      </mappers>

XML开发

基本流程

  • 编写核心配置文件 -> 替换连接信息,解决硬编码问题
  • 定义与SQL映射文件同名的Mapper接口,并且将Mapper接口和SQL映射文件放置在同一目录下【通过/分隔符在resources中创建dao目录,这样编译后的文件会在统一目录下】
  • 编写SQL映射文件,没置SQL映射文件的namespace属性为Mapper接口全限定名
    <?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="org.mybatis.example.BlogMapper">
      <select id="selectBlog" resultType="Blog">
        select * from Blog where id = #{id}
      </select>
    </mapper>
  • 在Mapper接口中定义方法,方法名就是SQL映射文件中sql语句的id,并保持参数类型和返回值类型一致

映射配置文件

  • insert: 映射插入语句
    <insert id="insertAuthor">
      insert into Author (id,username,password,email,bio)
      values (#{id},#{username},#{password},#{email},#{bio})
    </insert>
  • update: 映射更新语句
    • 更新对象
      <update id="updateAuthor">
        update Author set
          username = #{username},
          password = #{password},
          email = #{email},
          bio = #{bio}
        where id = #{id}
      </update>
    • 更新字段
      public abstract int updateAddressByName(@Param("sname") String name, @Param("saddress") String address);
      <update id="updateAddressByName">
      update student set stu_address = #{saddress}
      where stu_name = #{sname}
      </update>
  • delete: 映射删除语句
    <delete id="deleteAuthor">
      delete from Author where id = #{id}
    </delete>
  • select: 映射查询语句
    <select id="selectPerson" resultType="Person">
      SELECT * FROM persons WHERE id = #{id}
    </select>
  • cache: 该命名空间的缓存配置
  • cache-ref: 引用其它命名空间的缓存配置
  • sql: 可被其它语句引用的可重用语句块
    <sql id="userColumns">
        id, username, password, email
    </sql>
    <select id="getUserById" parameterType="int" resultType="User">
        SELECT
            <include refid="userColumns" />
        FROM
            users
        WHERE
            id = #{id}
    </select>
    <sql id="commonFields">
        <if test="true">
            id, `name`, tag, `status`, `path`, remark
        </if>
    </sql>

主键回填

  • 用途: 关联操作, 日志记录

  • 语法:

    <insert id="addOrder" useGeneratedKeys="true" keyProperty="id">
        insert into tb_order(payment, payment_type, status)
        values(#{payment},#{paymentType},#{status};
    </insert>

    • 注意: 主键回填到Order对象中,而返回值仍为受影响行数

动态SQL

  • <if>
    <select id="findBrandByCondition" resultMap="brandMap">
        SELECT id, brand_name, company_name, ordered, description, status
        FROM tb_brand
        WHERE 1=1
        <if test="brandName != null">
        AND brand_name = #{brandName}
        </if>
        <if test="companyName != null">
        AND company-name = #{companyName}
        </if>
        <if test="status != null">
        AND status = #{status}
        </if>
    </select>
  • <where>
    <select id="findBrandByCondition" resultMap="brandMap">
        SELECT id, brand_name, company_name, ordered, description, status
        FROM tb_brand
        <where>
            <if test="brandName != null">
            AND brand_name = #{brandName}
            </if>
            <if test="companyName != null">
            AND company-name = #{companyName}
            </if>
            <if test="status != null">
            AND status = #{status}
            </if>
        </where>
    </select>
  • <choose>
    <select id="selectByConditionSingle" resultMap="brandResultMap">
        select * from tb_brand
        where
        <choose>
            <when test="status != null">
                status = #{status}
            </when>
            <when test="companyName != null and companyName !=''">
                company_name like #{companyName}
            </when>
            <when test="brandName != null and brandName !=''">
                brand_name like #{brandName}
            </when>
            <otherwise>
            1=1
            </otherwise>
        </choose>
    </select>
  • set
    <update id="updateBrand">
        update tb_brand
        <set>
            <if test="brandName != null">
            brand_name = #{brandName},
            </if>
            <if test="companyName != null">
            company_name = #{companyName},
            </if>
        </set>
        where id = #{id}
    </update>
  • foreach
    public intenface BrandMapper{
        public int deleteBrandByIds(@Param("ids")List<Integer> ids);
    }
    <delete id="deleteBrandByIds">
        delete from tb_brand
        where id in
        <foreach collection="ids" item="id" separator="," open="(" close=")">
            #{id}
        </foreach>
    </delete>

resultMap

  • 解决查询结果字段名与实体类属性名不致的问题(别名映射)
    <resultMap id="brandMap" type="com.morningstar.pojo.Brand" autoMapping="true">
    <!-- id标签: 配置数据表的主健字段和实体类中的属性映射 -->
    <id column="id" property="id"></id>
    <!-- result标签: 配置数据表中非主健字段和实体类的属性映射 -->
    <result column="brand_name" property="brandName"></result>
    <result column="company_name" property="companyName"></result>
    </resultMap>
    <select id="findAllBrand" resultMap="brandMap">...</select>    
  • 解决多表查询关联映射【需要在pojo中配置好属性】
    • 一对一查询
      <resultMap id="orderMap" type="com.morningstar.pojo.Order">
          <!-- 配置:查询结果和Order类的映射关联 -->
          <id property="id" column="order_id"></id>
          <result property="orderNumber" column="order_number"></result>
          <association property="orderUser" javaType="com.morningstar.pojo.User" autoMapping="true">
          ...
          </association>
      </resultMap>
      <!-- 根据订单编号,查询订单信息及下单人信息 -->
      <select id="findOrderByNumber" resultMap="orderMap">
          select 
              tb_order.id AS order_id,
              tb-order.order-number,
              tb_user.id AS user_id,
              tb_user.user_name,
              tb_user. password,
              tb_user.name,
              tb_user.sex,
              tb_user.age
          from tb_order 
          inner join tb_user on tb_user.id = tb_order.user_id
          where tb_order.order_number= #{orderNumber}
      </select>
    • 一对多查询
      <resultMap id="userMap" type="com.morningstar.pojo.User" autoMapping="true">
          <id property="id" column="user_id"></id>
          <result property="userName" column="user_name"></result>
          <collection property="orders" javaType="java.util.List" ofType="com.morningstar.pojo.Order">
          ...
          </collection>
      </resultMap>

注解开发

  • 优点: 编写简单
  • 缺点: 不支持多表查询【只能多次查询】

基本流程

  • 编写核心配置文件
  • 定义Mapper接口
  • 通过注释实现接口
    @Insert("INSERT INTO tb_user (user_name, password, name, age, sex) " + "VALUES(#{userName}, #{password}, #{name}, #{age}, #{sex})")
    public int addUser(User user);

主键回填

@Insert("INSERT INTO tb_user(user_name, password, name, age, sex) "
+ "VALUES(#{userName}, #{password}, #{name}, #{age}, #{sex})")
@Options(useGeneratedKeys=true, keyColumn="id", keyProperty="id")
public int adduserGetPK(User user);

动态SQL

  • 只使用SqlProvider
    @SelectProvider(type=SqlProvider.class, method="findUserByName")
    List<User> findUserByName(@Param("uName") String name);
    public class SqlProvider{
        public String findUserByName(@Param("uName") String name){
            String sql="select from tb user where sex=1";
            if(name!=null){
                sql += " and user_name like concat('%', #{uName}, '%');
            }
            return sql;
        }
    }
  • 使用SqlProviderorg.apache.ibatis.jdbc.SQLSQL类语法复杂,不推荐使用】

@Results

  • 别名映射
    @Results({
        @Result(property="id", column="user_id", id=true),
        @Result(property="userName", column="user_name")
    })
  • 多表查询【推荐用XML处理表间关系】

参数处理

参数传递

  • 单个参数的情况: 可以直接在SQL中使用这个参数
    public interface UserMapper {
        @Select("SELECT id, username, password FROM users WHERE id = #{id}")
        User selectUserById(int id);
    }
  • 多个参数的情况: 需要使用@Param注解
    public interface UserMapper {
        @Select("SELECT id, username, password FROM users WHERE username = #{username} AND password = #{password}")
        User selectUserByUsernameAndPassword(@Param("username") String username, @Param("password") String password);
    }
  • 使用Map作为参数
    public interface UserMapper {
        @Select("SELECT id, username, password FROM users WHERE username = #{params.username} AND password = #{params.password}")
        User selectUserByUsernameAndPassword(Map<String, Object> params);
    }
    Map<String, Object> params = new HashMap<>();
    params.put("username", "myUsername");
    params.put("password", "myPassword");
    
    User user = userMapper.selectUserByUsernameAndPassword(params);
  • 使用对象作为参数
    public class UserQuery {
        private String username;
        private String password;
        // getters and setters
    }
    
    public interface UserMapper {
        @Select("SELECT id, username, password FROM users WHERE username = #{username} AND password = #{password}")
        User selectUserByUsernameAndPassword(UserQuery query);
    }
    UserQuery query = new UserQuery();
    query.setUsername("myUsername");
    query.setPassword("myPassword");
    
    User user = userMapper.selectUserByUsernameAndPassword(query);

参数解析

  • #{key}:
    • 作用: 在映射文件中做为sql的占位符参数使用
    • 区别:
      • 底层是使用PreparedStatement对象对sql进行预编译,可以防止SQL注入
      • 适用于参数传递
  • ${key}:
    • 作用:
      • 在核心配置文件中获取外部配置文件中的数据
      • 在映射文件中做为sql的占位符参数使用
    • 区别:
      • 底层是使用Statement对象,对sql进行拼接
      • 适用于动态SQL,比如表名、列名等【这种情况下无法用#{key}

JavaWeb

Tomcat

  • 作用: 作为容器提供HTTP环境供Java来运行,包括Servlet API, JSP API, WebSocket API
  • 安装: 推荐不要使用Docker安装,不方便与IDE联动
  • 结构:
    • webapps:
      • ROOT: 网站根目录
      • *: 其他路径的对应目录
        • WEB-INF: 网站配置以及私有资源
          • web.xml: 网站配置
          • classes: 编译的字节码文件
          • lib: 网站的依赖
    • conf:
      • server.xml: 服务器的相关配置,例如修改默认端口
        <Connector port="80" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"/>
      • web.xml: 整体的网站配置
    • work: 缓存中间产物,比如编译的JSP文件和序列化的会话数据,来提高性能
  • 注意: 一般采用idea集成的方式开发,此时tomcat运行于嵌入模式,除了webapp目录,其他的目录在System.getProperty("catalina.base")
    • mac为/Users/[username]/Library/Caches/JetBrains/IntelliJIdea[version]/tomcat/[uuid]

Servlet

  • 定义: Servlet是Java平台上的一种用于处理Web请求的API规范【相当于Python中的WSGI(Web Server Gateway Interface)】
  • 作用: 通过HTTP接受、处理和响应客户端请求【相当于MVC中的Controller】
  • 开发方式:
    • XML开发:
      • 导入依赖
        <dependency>
          <groupId>javax.servlet</groupId>
          <artifactId>javax.servlet-api</artifactId>
          <version>3.1.0</version>
        </dependency>
      • 自定义Servlet类并实现Servlet接口所有抽象方法,最重要的是service方法【自Tomcat10开始使用 jakarta.*命名空间】
        void service(ServletRequest req, SerVletResponse res);
      • 在web项目的核心配置文件web.xml中配置访问servlet的路径【tomcat会通过反射创建Servlet对象】
        <servlet>
            <servlet-name>Servlet0</servlet-name>
            <servlet-class>com.morningstar.practice.Servlet0</servlet-class>
        </servlet>
        <servlet-mapping>
            <servlet-name>Servlet0</servlet-name>
            <url-pattern>/hello</url-pattern>
        </servlet-mapping>
      • 启动tomcat【需要配置local环境】
    • 注解开发:【Servlet3.0开始支持】
      • 作用: 简化编写,增加可读性
      • 用法: 在Servlet实现类上添加注解@WebServlet("/test1/")
      • 属性:
        • name: 等价于<servlet-name>
        • value, urlPatterns: 等价于<url-pattern>
        • loadOnStartup: 等价于<load-on-startup>
        • initParams: 等价于<init-param>
  • 生命周期:【单例设计模式】
    • 创建:
      • 步骤:
        • 加载Servlet类及.class对应的数据
        • 创建ServletConfig对象
        • 创建Servlet对象
      • 创建者: tomcat(反射)
      • 创建时刻: 第一次访问对应url
        <load-on-startup>-1</load-on-startup>
        • 默认值为-1,当值大于等于0时,变为tomcat启动时创建
        • 值越大,创建优先级越高
      • 创建方法: 调用无参构造后,执行init方法初始化
    • 运行: 调用service(),构造servletRequest、servletResponse
    • 删除:
      • 删除时刻: tomcat关闭时
      • 删除访问: 调用destroy
  • 实现方式:
    • 直接实现javax.servlet.Servlet接口: 需要实现五个抽象方法
    • 继承javax.servlet.GenericServlet: 只需实现service方法
    • 继承javax.servlet.http.HttpServlet: 需要实现HTTP的请求方法
  • 常见错误:
    • 405 Method Not Allowed: 子类的请求方法中只调用了调用父类的请求方法,而没有实现具体的方法
    • 启动tomcat就报错:
      • <url-pattern>路径错误【没有写/、路径重复】
      • <servlet><servlet-mapping>中的<servlet-name>不对应
      • 项目的servlet-api没有导入,或与tomcat中的不兼容
  • 路径映射【Servlet和路径一对一或一对多,较Nginx的灵活性较差】
    • 完全路径映射: /xxx
    • 目录匹配: /xxx/**只匹配一截路径】
    • 后缀名匹配: *.xxx
    • 缺省路径:
      • /:
        • 匹配根路径【如果存在/index.html或者/index.jsp则不匹配】
        • 匹配不存在的路径
        • 匹配存在的静态路径的访问【问题严重,不建议配置】
      • /*: 与/类似,区别在于即便存在/index.html或者/index.jsp也会匹配根路径
    • 动态路径匹配高于静态资源路径匹配
  • 3.0新特性:
    • 支持ServletFilterListener注解配置
    • 支持Web模块: 可以将每个Servlet、Filter、Listener打成jar包,然后放在WEB-INF\lib
    • 异步处理
    • 文件上传API简化

Request

继承体系: ServletRequest(接口) -> HttpServletRequest(接口) -> org.apache.catalina.connector.Request(实现类) -> org.apache.catalina.connector.RequestFacade (包装类)

  • RequestFacade
    • 是一个包装类,持有一个Request实例,并将所有HttpServletRequest方法的调用委派给这个内部实例
    • 通过这种方式限制了客户端代码对实际请求对象的直接访问,增强了安全性和封装性

常见接口:

用法 drf中的Request Servlet中的Request
获取请求方法 .method getMethod()
获取上下文路径 reverse('api-root') getContextPath()
获取URI(只有路径) .path getRequestURI()
获取URL(包含三要素) .build_absolute_uri() StringBuffer getRequestURL()
获取查询参数(GET) .query_params[<name>] getParameter(<name>)String[] getParameterValues(<name>)
获取路径参数 kwargs.get(<name>) getPathInfo()
获取请求头 .headers.get(<name>) getHeader(String var1)
获取请求体(x-www-form-urlencoded) .data: QueryDict getParameter(<name>)String[] getParameterValues(<name>)
获取请求体(form-data) .data: QueryDict.FILES Part getPart(<name>)
获取请求体(json) .data: QueryDict 依赖第三方库
  • 上下文路径: 如果虚拟路径为"/", request.getContextPath()返回""
  • 查询参数: 通过getParameterMap()分离从getQueryString()得到的参数
  • 路径参数: Servlet中的Request只能获取匹配的路径,不能对其设置参数
  • 请求体: 通过ServletInputStream getInputstream()BufferedReader getReader()解析
    • form-data请求体的处理需要配置@MultipartConfig,且解析较为复杂
  • ParameterMap处理方式: 先加载请求参数中的键值对,再加载请求体中的键值对

编码问题:

  • 原因: tomcat默认编码("ISO-8859-1")与http请求体所用编码("UTF-8")不一致【tomcat8之后才可以设置URIEncoding属性】
  • 解决:
    • 设置请求体的编码:
      request.setCharacterEncoding("UTF-8")
    • 对错误解码后的结果先编码后解码:
      URLDecoder.decode(URLEncoder.encode(request.getParameter(<name>), "ISO-8859-1"), "UTF-8")
      new String(request.getParameter(<name>).getBytes("ISO-8859-1"), "UTF-8")

Response

继承体系: ServletResponse(接口) -> HttpServletResponse(接口) -> org.apache.catalina.connector.Response(实现类) -> org.apache.catalina.connector.ResponseFacade (包装类) - ResponseFacade: 类似RequestFacade

常用接口:

用法 drf中的Response Servlet中的Request
设置状态码 初始化传参status=status.HTTP_200_OK setStatus(HttpServletResponse.SC_OK)
设置响应头 response[<name>] = <value> setHeader(<name>, <value>)
设置响应体(json) Response(serializer.data) 依赖第三方库
  • 响应体: Servlet中通过ServletOutputStream getOutputstream()PrintWriter getWriter()设置
    • 字符响应:
      • 基本用法:
        response.getWriter().print("你好")
      • 注意编码问题
        response.setHeader("content-type", "text/html;charset=utf-8"); 
        response.setContentType("text/plain");
        response.setCharacterEncoding("UTF-8");  
    • 字节响应:
      • 基本用法:
        try(InputStream bis = getServletContext().getResourceAsStream("/img/训练流程图.pdf");) {
            ServletOutputStream sos = response.getOutputStream();
            byte[] buffer = new byte[1024];
            int len = 0;
            while((len=bis.read(buffer))!=-1){
                sos.write(buffer,0,len);
            }
        }

请求转发

  • 作用: 将请求从一个Servlet转发到另一个,而不让客户端知道内部的转发过程,从而分离业务逻辑和显示逻辑
  • 用途:
  • 实现:
    • 请求转发:【没有转交控制权】
      RequestDispatcher dispatcher request.getRequestDispatcher("/path");
      dispatcher.forward(request, response);
    • 资源共享: 通过在不同的servlet中分别使用request.setAttribute(<name>)request.getAttribute(<name>)实现共享request域对象数据
  • 对比重定向【可用.sendRedirect(location)实现临时重定向】: | | 请求转发 | 重定向 | |:---|:---:|:---:| | 地址栏 | 不发生改变 | 发生改变 | |跳转类型好| 服务器内部跳转|服务器外部跳转| |请求响应次数|一次请求一次响应|两次请求两次响应| |request域数据|可以共享|不能共享|

会话追踪

  • Cookie:
    • 基本使用:
      • 添加:
        Cookie cookie = new Cookie("name", "jp");
        response.addCookie(cookie);
      • 获取:
        Cookie[] cookies = request.getCookies();
        for (Cookie cookie: cookies) {
            System.out.println(cookie.getName() + "=" + cookie.getValue());
        }
    • 原理: 通过响应头中的Set-Cookie(可以不唯一)和请求头中的Cookie来传递
    • 细节:
      • 设置存活时间: cookie.setMaxAge(int expiry)【默认为会话级别】
        • 负数: 存储到浏览器内存中
        • 零: 立即删除
        • 正数: 将数据持久化存储
      • 特殊字符问题: 空格之类的特殊字符需要使用URL编解码
      • 设置有效路径: cookie.setPath("/path")
      • HTTPOnly: cookie可以设置HTTPOnly避免XSS攻击
  • Session
    • 基本使用:
      HttpSession session = request.getSession(true); // 无参也为true,false代表不自动创建
      session.setAttribute("name", 24);
      System.out.println(session.getAttribute("name"));
    • 原理: tomcat为每个Session容器确定唯一的JSESSIONID,并为其创建一个会话级别的cookie发送到浏览器
    • 细节:
      • 持久化session:
        HttpSession session = request.getSession();
        Cookie cookie = new Cookie("JSESSIONID", session.getId());
        cookie.setMaxAge(60*60*24);
        response.add(cookie);
      • session钝化和活化: 通过序列化和反序列化存储和读取TOMCAT_HOME/work/Catalina/[path]/SESSIONS.ser内的session数据【开发/嵌入模式中不启用此功能】
      • session的销毁
        • 通过web.xml设置<session-timeout>【默认为30分钟】
        • 通过invalidate()销毁
    • 最佳实践: 一般采用惰性创建,只有在尝试写入session数据或者用户登录后,才创建session数据

JSP

基本概念

  • 定义: java server pages,本质是一个Servlet类【定位类似于django template,位于MVC中的View】
  • 执行方法: tomcat执行jsp的时候会将jsp翻译成java,然后在编译为.class文件,最后运行.class文件
    • 路径: System.getProperty("catalina.base")
    • 名称: [filename]_jsp.java[filename]_jsp.class
  • 依赖: javax.servlet.jsp: javax.servlet.jsp-api
  • 对比JSPX: JSPX中关于java代码的部分全部使用特定的标签,更符合XML,方便解析与排错

简单示例

<%@ page import="java.util.Date" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%--
  Created by IntelliJ IDEA.
  User: henry529
  Date: 2024/6/4
  Time: 09:50
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<h1>
    这是个标题
</h1>
<body>
<%--JSP脚本片段--%>
<%
    Date date = new Date();
%>
<%--JSP声明--%>
<%!
    public String formatDate(Date date) {
        SimpleDateFormat spf = new SimpleDateFormat("yyyy-MM-dd");
        return spf.format(date);
    }
%>
<%--JSP表达式--%>
<%=formatDate(date)%>
</body>
</html>

JSP指令

<%@ 指令名 属性名="属性值" 属性名="属性值" 属性名="属性值"%>
  • page: 设置contentType和语言、导包以及设置错误页面
    <%@page contentType="text/html;charset=UTF-8" language="java" %>
    <%@page import="java.util.Date" %>
    <%@page errorPage="/error. jsp"%>
    • 编写错误页面
      <%@page isErrorPage="true" %>
      <%
          String message = exception.getMessage();
          response.getwriter().print(message);
      %>
  • taglib: 导入第三方标签库
    <%@ taglib uri="标签库地址" prefix="标签前缀" %>
  • include: 在—个jsp页面静态包含另一个jsp页面
    <%@ include file="引入的jsp地址"%>

错误页面配置

webapp/WEB-INF/web.xml中配置:

<error-page>
    <error-code>404</error-code>
    <location>/404.jsp</location>
</error-page>
<error-page>
    <exception-type>java.lang.NullPointerException</exception-type>
    <location>/null.jsp</location>
</error-page>

内置对象

  • 原因: 编译后的代码部分都放在_jspService中,因此可以使用其中存在的内置对象
  • 分类:
    • request(HttpServletRequest)
    • response(HttpServletResponse)
    • session(HttpSession)
    • pageContext(PageContext): 比request还小的作用域
    • application(ServletContext)
      • 调用: ServletContext servletContext = getServletContext()
      • 使用:
        • 作为域对象传递数据
        • 获取上下文路径: servletContext.getContextPath()【等价于request.getContextPath()
        • 获取文件MIME类型: servletContext.getMimeType(<path>)
        • 获取文件真实路径: servletContext.getRealPath(<path>)
    • page/this(HttpServlet)
    • out(JSPWriter):
      • 在每次请求结束时自动刷新它的输出缓冲区
    • config(Servletconfig)
    • exception(Throwable): 错误页面才有
  • 使用: EL表达式
    • 确保启用EL: <%@ page isELIgnored="false" %>
    • 获取数据:
      • 从jsp域对象(pageContext,request,session,application)中获取数据: ${[scope].[key]}
      • 从cookies中获取数据: ${cookie.<name>.value}
    • 相关操作
      • 算术、关系、逻辑、三元运算
      • emptynot empty【空字符串、空集合、空对象】

JSTL

  • 定义: (The JavaServer Pages Standard Tag Library): 解决java代码与前端代码耦合的问题
  • 依赖: javax.servlet: jstl
  • 用法:
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
  • 常用标签
    • <c:out>: 通常用于输出一段文本内容到客户端浏览器
      <c:out value="123" />
    • <c:set>: 用于设置各种Web域中的属性【默认作用域为page
      <c:set var="变量名" value="值" [scope="变量作用域"] />
    • <c:remove>: 用于删除各种Web域中的属性
      <c:remove var="变量名" scope="变量作用域" />
    • <c:catch>: 用于捕获嵌套在标签体中的内容抛出的异常
    • <c:if>: java代码if00语句功能
      <c:if test="${requestScope.msg!=null}">
      if is true
      </c:if>
    • <c:choose>: 用于指定多个条件选择的组合边界,它必须与<c:when><c:otherwise>标签一起使用
    • <c:forEach>: 代替java代码for循环语句
      <c:forEach var="x" items="数组名或者集合名">
      ${x}
      </c:forEach>
      <c:forEach var="x" items="map名">
      ${x.key}, ${x.value} 
      </c:forEach>
    • <c:forTokens>: 迭代操作String字符
    • <c:param>: 给请求路径添加参数
    • <c:url>: 重写url,在请求路径添加sessionid
    • <c:import>: 用于在JSP页面中导入一个URL地址指向的资源内容
    • <c:redirect>: 用于将当前的访问请求转发或重定向到其他资源

过滤器(Filter)

  • 来源: javax.servlet.Filter接口
  • 作用: 对用户发送的请求或响应进行集中处理,实现请求的拦截
  • 场景:
    • 解决post请求中文乱码(全站)
    • 敏感字符过滤
    • 登录权限进行校验
  • 方法:
    • void init(FilterConfig filterConfig)
    • void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    • void destory()
  • 使用:
    • 实现接口
      package com.morningstar.javaweb;
      
      import javax.servlet.*;
      import java.io.IOException;
      
      public class MyFilter implements Filter {
          @Override
          public void init(FilterConfig filterConfig) throws ServletException {
          }
      
          @Override
          public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
              System.out.println("Do Filter");
              filterChain.doFilter(servletRequest, servletResponse);
          }
      
          @Override
          public void destroy() {
          }
      }
      • filterChain.doFilter(servletRequest, servletResponse): 当前过滤器放行,调用下一个过滤器或链尾资源
    • 配置xml映射或者注解
      <?xml version="1.0" encoding="UTF-8"?>
      <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
               version="4.0">
          <filter>
              <filter-name>MyFilter</filter-name>
              <filter-class>com.morningstar.javaweb.MyFilter</filter-class>
          </filter>
          <filter-mapping>
              <filter-name>MyFilter</filter-name>
              <url-pattern>/index.jsp</url-pattern>
          </filter-mapping>
      </web-app>
      @WebFilter("/") // 类似@WebServlet
  • 声明周期:
    • 创建: tomcat启动时先调用无参构造,后调用init方法【通过反射创建】
    • 执行: 每次访问对应路径都执行doFilter
    • 销毁: tomcat关闭
  • 拦截路径: 与Servlet路径映射类似,但/只匹配根路径
  • 过滤器链:
    • 过滤顺序: 与匹配优先级无关
      • 如果是xml配置,按照xml中<filter-mapping>顺序执行

监听器(Listener)

  • 来源: (javax.servlet.XxxListener)
  • 作用: 在Web执行过程中,监听一些事件,当相应事件发生时,进行处理
  • 原理: 监听ServletContext, HttpSession, HttpServletRequest三个对象创建和销毁的,同时监听是哪个对象中数据的变化
  • 分类:
    • ServletContextListener
    • ServletContextAttributeListener
    • HttpSessionListener
    • HttpSessionAttributeListener
    • ServletRequestListener
    • ServletRequestAttributeListener
  • 使用:
    • 典型案例: 加载初始化参数(类似Spring框架)
      • 创建实现类
        public class MyServletContextListener implements ServletContextListener {
            @Override
            public void contextInitialized(ServletContextEvent sce) {
                ServletContext servletContext = sce.getServletContext();
                String configFilename = servletContext.getInitParameter("jdbc");
                Properties properties = new Properties();
                String configPath = "/WEB-INF/classes/"+ configFilename;
                try(InputStream inputStream = servletContext.getResourceAsStream(configPath);) {
                    properties.load(inputStream);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                String driverName = properties.getProperty("jdbc.driver");
                System.out.println(driverName);
                System.out.println("ServletContext创建了");
            }
            @Override
            public void contextDestroyed(ServletContextEvent sce) {
                System.out.println("ServletContext销毁了");
            }
        }
      • 通过XML引入项目
        <listener>
            <listener-class>com.morningstar.javaweb.MyServletContextListener</listener-class>
        </listener>
        
        <context-param>
            <param-name>jdbc</param-name>
            <param-value>jdbc.properties</param-value>
        </context-param>
    • 统计在线人数
      @WebListener
      public class InitNumberListener implements ServletContextListener {
      
          @Override
          public void contextInitialized(ServletContextEvent servletContextEvent) {
              // 获取上下文域对象
              ServletContext servletContext = servletContextEvent.getServletContext();
              // 初始化在线人数
              servletContext.setAttribute("number", 0);
          }
      
          @Override
          public void contextDestroyed(ServletContextEvent servletContextEvent) {
      
          }
      }
      @WebListener
      public class NumberChangeListener implements HttpSessionListener {
      
          // 会话建立,在线人数+1
          @Override
          public void sessionCreated(HttpSessionEvent httpSessionEvent) {
              // 获取session域对象
              HttpSession session = httpSessionEvent.getSession();
              // 获取上下文域对象
              ServletContext servletContext = session.getServletContext();
              // 取出在线人数
              Integer number = (Integer) servletContext.getAttribute("number");
              // +1
              servletContext.setAttribute("number", number + 1);
          }
      
          // 会话销毁,在线人数-1
          @Override
          public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
              // 获取session域对象
              HttpSession session = httpSessionEvent.getSession();
              // 获取上下文域对象
              ServletContext servletContext = session.getServletContext();
              // 取出在线人数
              Integer number = (Integer) servletContext.getAttribute("number");
              // -1
              servletContext.setAttribute("number", number - 1);
          }
      }

### JSON处理

  • 工具库: jacksonfastjson
  • fastjson: com.alibaba:fastjson
    • 方法:
      • toJSONString(Object object)
      • T parseObject(string text, Class<T> clazz)
      • T parseObject(Inputstream is, Class<T> clazz)
    • 使用:
      User user = JSON.parseobject(request.getInputstream(),  User.class);
      String jsonStr = JSON.toJSONString(user)
  • jackson: com.fasterxml.jackson.core:jackson-annotations【基于data-bind
    • 转换:
      User user = new User("小明");
      ObjectMapper objectMapper = new ObjectMapper();
      try {
          // 将User对象转换为JSON字符串
          String jsonString = objectMapper.writeValueAsString(user);
          log.info(jsonString);
      } catch (Exception e) {
          log.error(e.getMessage());
      }
    • 配置:
      • @JsonInclude(JsonInclude.Include.NON_NULL): 对象json序列化时,忽略为null的属性
      • @JsonIgnore: 作用的类|属性不参与json序列化
      • @JsonSerialize(using = ToStringSerializer.class): 指定属性序列化格式
      • @JsonFormat(pattern = "yyyy-MM-dd"): json格式序列化时指定日期对象转字符串格式

路径模块化

  • BaseServlet
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.lang.reflect.Method;
    
    public class BaseServlet extends HttpServlet {
        protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            doGet(request, response);
        }
    
        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            String uri = request.getRequestURI();        
            int lastIndex = uri.lastIndexOf("/");
            String methodName = uri.substring(lastIndex + 1);
            try {
                Class<?> clazz = this.getClass();
                Method m = clazz.getMethod(methodName, HttpServletRequest.class, HttpServletResponse.class);
                m.invoke(this,request,response);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
  • 具体的Servlet
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    @WebServlet("/user/*")
    public class UserServlet extends BaseServlet {
        public void delete(HttpServletRequest request, HttpServletResponse response) {
            System.out.println("删除用户");
        }
        public void add(HttpServletRequest request, HttpServletResponse response) {
            System.out.println("添加用户");
        }
        public void update(HttpServletRequest request, HttpServletResponse response) {
            System.out.println("更新用户");
        }
        public void findAll(HttpServletRequest request, HttpServletResponse response) {
            System.out.println("查询所有用户");
        }
    }

SPI机制

  • 定义: (Service Provider Interface) 用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件
  • 作用: 可以通过ServiceLoader装载的是一系列有某种共同特征的实现类(类实现同一个接口),将一些类信息写在约定的文件中
  • 用法: 在实现类的工程中的resources/META-INF/services目录下存在接口同名的文件,里面包含实现类的全路径【接口编程 + 策略模式 + 配置文件】
  • 场景:
    • 场景1: 两个项目中的HondaCarTeslaCar分别实现了Car接口,在另一个项目中同时测试这两个实现类
      ServiceLoader<Car> cars = ServiceLoader.load(Car.class);
      for(Car car: cars){
          System.out.printin(car.getColor());
      }
    • 场景2: JDBC4.0免注册驱动【所有导入的驱动依赖包只要支持JDBC就会自动被导入】
    • 场景3: 日志门面SLF4J接口实现类自动加载
    • 场景4: ServletContainerInitializer【Servlet3.0之后】
      • 作用: 主要用于在容器启动阶段通过编程风格注册Filter, Servlet以及Listener 以取代通过web.xml配置注册【支持注解且更加灵活】
      • 原理:
        • ServletContainerInitializer接口的实现类通过java SPI声明自己是ServletContainerInitializer的提供者
        • web容器启动阶段依据java SPI获取到所有ServletContainenInitializer的实现类,然后执行其onstartup方法
        • onStartup中通过编码方式将组件Servlet加载到ServletContext
      • 使用:
        public class MyServletContainerInitializer implements ServletContainerInitializer {
            @Override
            public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
                System.out.println("start..........................");
                MyServlet myServlet = new MyServlet();
                ctx.addServlet("myServlet", myServlet).addMapping("/my");
            }
        }

Spring FrameWork

  • 发展经历【5.0支持java8】
  • 框架演变【4.0已经稳定】
  • 系统架构
    • Data Access: 数据访问
    • Data Integration: 数据集成
    • AOP: (Aspect-Oriented Programming) 面向切面编程
    • Aspects: AOP思想的实现
    • Core Container: 核心容器
    • Test: 单元测试和集成测试
  • 学习路线

IoC与DI

基本概念

  • IoC:
    • 定义: Inversion of Control(控制反转) 由主动new对象转换为由"外部"提供对象
    • 效果: 使用对象时不仅可以直接从IoC容器中获取,并且获取到的bean已经绑定了所有的依赖关系
    • 作用:
      • 减少代码耦合、提高可测试性、增强灵活性
      • 配置复杂性、过度工程
    • 实现:
      • Spring提供了一个IoC容器,用来充当IoC中的"外部"
      • IoC容器负责对象的创建、初始化等一系列工作,被创建或被管理的对象在Ioc容器中统称为Bean
    • 其他案例:
      • 事件驱动模型【e.g., 调试器调试运行系统是通过运行系统通知调试器来实现的】
  • DI:
    • 定义: Dependency Injection(依赖注入) 在容器中建立bean与bean之间的依赖关系

模拟实现

反射+面向接口编型+读取配置文件+工厂设计模式等【SpringIOC容器底层是map集合】

import java.util.HashMap;
import java.util.ResourceBundle;

public class BeansFactory {
    /*
        key            value
        userService    UserServiceImpl0x001
        roleService    RoleServiceImpl0x002
     */
    private static HashMap<String,Object> map = new HashMap<>();
    public static synchronized <T> T getInstance(String key) throws Exception{
        Object obj = map.get(key);
        if(obj == null){
            ResourceBundle bundle = ResourceBundle.getBundle("beans");
            String classNameStr = bundle.getString(key);
            Class<?> clazz = Class.forName(classNameStr);
            obj = clazz.newInstance();
            map.put(key,obj);
        }
        return (T)obj;
    }
}

Spring容器

  • 容器类层次:
    • BeanFactory: IoC容器的顶层接口,初始化BeanFactory对象时,加载的bean延迟加载
    • ApplicationContext: Spring容器的核心接口,初始化时bean立即加载【继承自BeanFactory
      • 作用: 提供基础的bean操作相关方法,通过其他接口扩展其功能
      • 常用初始化类: ClassPathXmlApplicationContext, FileSystemXmlApplicationContext
  • 容器创建:
    • 类路径加载配置文件: ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    • 文件路径加载配置文件: ApplicationContext ctx = new FileSystemXmlApplicationContext("D:\\applicationContext.xml");
    • 加载多个配置文件: ApplicationContext ctx = new ClassPathXmlApplicationContext("bean1.xml", "bean2.xml");
  • Bean获取:
    • 使用bean名称获取: BookDao bookDao = (BookDao) ctx.getBean("bookDao");
    • 使用bean名称获取并指定类型: BookDao bookDao = ctx.getBean("bookDao", BookDao.class);
    • 使用bean类型获取: BookDao bookDao = ctx.getBean(BookDao.class);

XML开发

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="bookDao" class="com.morningstar.dao.impl.BookDaoImpl" scope="singleton" />
    <bean id="bookService" class="com.morningstar.service.impl.BookServiceImpl">
        <property name="bookDao" ref="bookDao"/>
    </bean>
</beans>
ApplicationContext ctx = new ClassPathXmlApplicationContext("springContext.xml");
BookService bookService = (BookService) ctx.getBean("bookService");
bookService.save();

  • name: 别名【id也是name
    <bean id="myBean" name="bean1, bean2" class="com.example.MyBeanClass">
    </bean>
  • scope: 分为singletonprototyperequestsessionapplicationwebsocket
    <bean id="bookDao" class="com.morningstar.dao.impl.BookDaoImpl" scope="prototype"></bean>
    • 适合交给容器进行管理的bean
      • 表现层对象
      • 业务层对象
      • 数据层对象
      • 工具对象
    • 不适合交给容器进行管理的bean
      • 封装实体的域对象
  • lazy-init="true": 延时加载
  • 实例化手段:
    • 无参构造
    • 静态工厂方法
      <bean id="bookDao1" class="com.morningstar.factory.StaticBookFactory" factory-method="getBookDao" />
    • 实例工厂方法
      • 原始
        <bean id="instanceBookFactory" class="com.morningstar.factory.InstanceBookFactory"/>
        <bean id="bookDao2" factory-bean="instanceBookFactory" factory-method="getInstanceBookDao"/>
      • 先进
        import com.morningstar.dao.impl.BookDaoImpl;
        import org.springframework.beans.factory.FactoryBean;
        
        public class MyBookFactory implements FactoryBean {
            @Override
            public Object getObject() throws Exception {
                return new BookDaoImpl();
            }
        
            @Override
            public Class<?> getObjectType() {
                return BookDaoImpl.class;
            }
        }
        <bean id="bookDao3" class="com.morningstar.factory.MyBookFactory"/>
  • 依赖注入方式
    • setter注入
      <bean id="bookService" class="com.morningstar.service.impl.BookServiceImpl">
          <property name="bookDao" ref="bookDao"/>
          <property name="number" value="10"/>
      </bean>
    • 带参构造注入
      <bean id="bookService" class="com.morningstar.service.impl.BookServiceImpl">
          <constructor-arg name="bookDao" ref="bookDao"/>
          <constructor-arg name="number" value="10"/>
      </bean>
    • 自动装配: IoC容器根据bean所依赖的资源在容器中自动查找并注入到bean中的过程【优先级低于手动】
      <bean id="bookService" class="com.morningstar.service.impl.BookServiceImpl" autowire="byType">
      • 方式: 按类型("byType"), 按名称("byName")
    • setter注入 or 带参构造注入:
      • 强制依赖使用构造器进行,可选依赖使用setter注入
      • Spring框架倡导使用构造器,第三方框架内部大多数采用构造器注入的形式进行数据初始化,相对严谨
      • 如果受控对象没有提供setter方法就必须使用构造器注入
  • 集合类型注入:
    • 数组:
      <property name="array">
          <array>
              <value>value1</value>
              <ref bean="item2"/>
          </array>
      </property>
    • List:
      <property name="list">
          <list>
              <value>value1</value>
              <ref bean="item2"/>
          </list>
      </property>
    • Set:
      <property name="set">
          <set>
              <value>value1</value>
              <ref bean="item2"/>
          </set>
      </property>
    • Map:
      <property name="map">
          <map>
              <entry key="key1" value="value1"/>
              <entry key="key2" value-ref="item2"/>
          </map>
      </property>
    • Properties:
      <property name="properties">
          <props>
              <prop key="key1">value1</prop>
              <prop key="key2">value2</prop>
          </props>
      </property>
  • 生命周期及其控制:
    • 周期:
      • 初始化容器
        • 创建对象(内存分配)
        • 执行构造方法
        • 执行属性注入(set操作)
        • 执行bean初始化方法
      • 使用bean: 执行业务操作
      • 关闭/销毁容器
        • 执行bean销毁方法
    • 控制:
      • 手动:
        <bean id="bookDao" class="com.morningstar.dao.impl.BookDaoImpl" scope="singleton" init-method="init" destroy-method="destory"/>
      • 自动: 实现InitializingBeanDisposableBean接口后就可以自动适配对应钩子
        public class BookDaoImpl implements BookDao, InitializingBean, DisposableBean {
            @Override
            public void save() {
                System.out.println("BookDaoImpl save");
            }
            @Override
            public void destroy() throws Exception {
            }
            @Override
            public void afterPropertiesSet() throws Exception {
            }
        }
    • 注意:
      • 关闭:
        • ApplicationContext没有提供close()方法,无法自动调用destory
        • ClassPathXmlApplicationContext支持close(),但更推荐使用registerShutdownHook()
  • 开启context命名空间
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
    ">
    </beans>
  • 读取properties
    <context:property-placeholder location="jdbc.properties"/>
    • 不加载系统属性: <context:property-placeholder location="jdbc.properties" system-properties-mode="NEVER"/>
    • 加载多个properties文件: <context:property-placeholder location="jdbc.properties,msg.properties"/>
    • 加载所有properties文件: <context:property-placeholder location="*.properties"/>
    • 加载properties文件标准格式: <context:property-placeholder location="classpath:*.properties"/>
    • 从类路径或jar包中搜索并加载properties文件: <context:property-placeholder location="classpath*:*.properties"/>
  • 第三方资源配置
    <bean id="druid" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3307/testdb?allowMultiQueries=true"/>
        <property name="username" value="root"/>
        <property name="password" value="1234asdw"/>
    </bean>
    <bean id="c3p0" class="com.mchange.v2.c3p0.jboss.C3P0PooledDataSource">
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
        <property name="user" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

注解开发

  • xml结合注解
    <context:component-scan base-package="com.morningstar.dao"/>
    @Component("bookDao") 
    public class BookDaoImpl implements BookDao {
    }
    ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("springContext.xml");
    ctx.getBean("bookDao"); // 类名按照驼峰规则的,默认是类名首字母小写
    ctx.getBean(BookDao.class);
    • @Component: 按功能可以细分为
      • @Controller: 用于表现层bean定义
      • @Service: 用于业务层bean定义
      • @Repository: 用于数据层bean定义
  • 纯注解
    @Repository("bookDao")
    @Scope("singleton")
    public class BookDaoImpl implements BookDao {
        @Override
        public void save() {
            System.out.println("BookDaoImpl save");
        }
        @PostConstruct
        public void init(){
            System.out.println("init...");
        }
        @PreDestroy
        public void destory(){
            System.out.println("destory...");
        }
    }
    @Service("bookService")
    public class BookServiceImpl implements BookService {
        @Autowired
        @Qualifier("bookDao")
        private BookDao bookDao;
        @Value("10")
        private Integer number;
        @Value("${name}")
        private String name;
        @Override
        public void save() {
            bookDao.save();
            System.out.println("BookServiceImpl save success");
        }
    }
    @Configuration
    public class JdbcConfig {
        @Value("${jdbc.driver}")
        private String driver;
        @Value("${jdbc.url}")
        private String url;
        @Value("${jdbc.username}")
        private String username;
        @Value("${jdbc.password}")
        private String password;
        @Bean("dataSource")
        public DataSource getDataSource(BookDao bookDao) {
            System.out.println("dataSource bookDao = " + bookDao.toString());
            DruidDataSource druidDataSource = new DruidDataSource();
            druidDataSource.setDriverClassName(driver);
            druidDataSource.setUrl(url);
            druidDataSource.setUsername(username);
            druidDataSource.setPassword(password);
            return druidDataSource;
        }
    }
    package com.morningstar.config;
    @Configuration
    @ComponentScan({"com.morningstar", "com.morningstar.service", "com.morningstar.config"})
    @PropertySource({"classpath:jdbc.properties", "book.properties"})
    //@Import(JdbcConfig.class)
    public class SpringConfig {
    }
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
    ctx.getBean(BookDao.class);
    BookService bookService = (BookService) ctx.getBean("bookService");
    bookService.save();
    DataSource dataSource = (DataSource) ctx.getBean("dataSource");
    • @Autowired: 先按照类型、后按照名称进行匹配【目前更推荐用@RequiredArgsConstructor(onConstructor = @__(@Autowired))实现构造器注入】
      1. 默认先byType,根据类型找到需要的对象,放入一个Map中
      2. 判断Map的size不大于1,则无需走下边判断,可以直接注入;大于1,spring会判断是否有@Primary@Priority,如果都没有,会比较属性名与找 到的对象的beanName【需要@Qualifier
    • @Bean:
      • 管理第三方的Bean,支持从IoC容器获取形参对象
      • 也可以管理自己开发的类【但一般只有在用工厂方法创建bean的情况下使用】
    • @PostConstruct@PreDestroy注解是jdk中提供的注解,从jdk9开始,jdk中的javax.annotation包被移除了,可以额外导入一下依赖解决这个问题
      <dependency>
        <groupId>javax.annotation</groupId>
        <artifactId>javax.annotation-api</artifactId>
        <version>${annotation.version}</version>
      </dependency>

整合Mybatis

  • 依赖:
    • org.springframework: spring-context
    • org.springframework: spring-jdbc
    • org.mybatis: mybatis
    • org.mybatis: mybatis-spring
    • com.alibaba: druid
    • mysql: mysql-connector-java
  • SQL配置
    create database if not exists testdb;
    use testdb;
    create table if not exists students (
        id int primary key AUTO_INCREMENT,
        name varchar(10) not null,
        classId int default 1,
        age int,
        birthday date,
        sex char(1) default '男',
        address varchar(50)
    );
    insert into students(name, age, birthday, sex) values ('吉普', 25, '1999-10-15','男'), ('布朗', 1, '2023-01-30', '女'), ('巧玲', 22, '2002-05-21', '女');
  • com.morningstar.config.JdbcConfig
    package com.morningstar.config;
    
    import com.alibaba.druid.pool.DruidDataSource;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.sql.DataSource;
    
    @Configuration
    public class JdbcConfig {
        @Value("${jdbc.driver}")
        private String driver;
        @Value("${jdbc.url}")
        private String url;
        @Value("${jdbc.username}")
        private String username;
        @Value("${jdbc.password}")
        private String password;
        @Bean("dataSource")
        public DataSource getDataSource() {
            DruidDataSource druidDataSource = new DruidDataSource();
            druidDataSource.setDriverClassName(driver);
            druidDataSource.setUrl(url);
            druidDataSource.setUsername(username);
            druidDataSource.setPassword(password);
            return druidDataSource;
        }
    }
  • com.morningstar.config.MybatisConfig
    package com.morningstar.config;
    
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.mapper.MapperScannerConfigurer;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import javax.sql.DataSource;
    
    /**
     * @Description Mybatis 配置类:创建SqlSessionFactory
     */
    @Configuration
    public class MybatisConfig {
        /**
         * 实例化sqlSessionFactory
         *
         * @param dataSource
         * @return org.mybatis.spring.SqlSessionFactoryBean
         */
        @Bean
        public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setTypeAliasesPackage("com.morningstar.po");
            sqlSessionFactoryBean.setDataSource(dataSource);
            return sqlSessionFactoryBean;
        }
    
        /**
         * 创建 MapperScannerConfigurer 对象,加载dao层接口
         * @return org.mybatis.spring.mapper.MapperScannerConfigurer
         */
        @Bean
        public MapperScannerConfigurer mapperScannerConfigurer() {
            MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
            mapperScannerConfigurer.setBasePackage("com.morningstar.dao");
            return mapperScannerConfigurer;
        }
    }
  • com.morningstar.config.SpringConfig
    package com.morningstar.config;
    
    import org.springframework.context.annotation.*;
    
    @Configuration
    @ComponentScan({"com.morningstar.service", "com.morningstar.config"})
    @PropertySource({"classpath:jdbc.properties"})
    public class SpringConfig {
    }

整合junit

  • 依赖:
    • org.springframework: spring-context
    • org.springframework: spring-test
    • junit: junit(需要4.x.x版本)
  • 使用:
    package com.morningstar.dao;
    
    //添加Spring整合junit的专用类加载器
    @RunWith(SpringJUnit4ClassRunner.class)
    //加载Spring配置类
    @ContextConfiguration(classes = SpringConfig.class)
    public class StudentMapperTest {
        @Autowired
        private StudentMapper studentMapper;
    
        @Test
        public void testFindById(){
            Student account = studentMapper.findById(2);
            System.out.println(account);
        }
    }

AOP

基础知识

  • 定义: (Aspect Oriented Programming) 面向切面编程,一种编程范式,指导开发者如何组织程序结构
  • 作用: 在不改动原始设计的基础上为其进行功能增强【类似装饰器模式,但粒度不同】
  • 本质: 代理模式
  • 场景:
    • 在工程运行慢的过程中,对目标方法进行运行耗时统计
    • 对目标方法添加事务管理
    • 对目标方法添加权限访问控制
  • 优点:
    • 简化开发
    • 灵活性强
    • Spring事务使用AOP实现
  • 依赖: org.aspectj:aspectjweaver
  • 核心概念:
    • 连接点(JoinPoint): 程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
      • 在SpringAOP中,理解为方法的执行
    • 通知类/切面类: 定义通知的类
      • 切入点(Pointcut): 匹配连接点的式子
        • 在SpringAOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法
          • 一个具体方法: com.morningstar.dao包下的BookDao接口中的无形参无返回值的save方法
          • 匹配多个方法:
            • 所有的save方法
            • 所有的get开头的方法
            • 所有以Dao结尾的接口中的任意方法
            • 所有带有一个参数的方法
      • 切面(Aspect): 描述通知与切入点的对应关系
        • 通知(Advice): 在切入点处执行的操作,也就是共性功能
          • 在SpringAOP中,功能最终以方法的形式呈现
    • 目标对象与代理:
      • 区分:
        • 目标对象(Target): 原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
        • 代理(Proxy): 目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现
      • 现象:
        • 如果UserDaoImpl类中的方法被增强了,容器中不会存在UserDaoImpl.class,只会存在它的代理对象,只能使用UserDao.class或者名称来访问
    • 织入: 将增强添加对目标类具体连接点上的过程
      • 编译期织入: 要求使用特殊的Java编译器
      • 类装载期织入: 这要求使用特殊的类装载器
      • 动态代理织入: 在运行期为目标类添加增强生成子类的方式【Spring默认】
  • 相关注解:
    • @Aspect: 定义切面类
    • @Pointcut: 定义切入点
    • @Before,@After...: 设置当前通知方法与切入点之间的绑定关系
    • @EnableAspectJAutoProxy: 在SpringConfig上开启AOP功能
  • 相似技术对比:
    • AOP: 函数/方法级别、依赖框架
    • Python装饰器: 函数/方法级别、语言特性、通知和连接点分开
    • 动态代理: 对象/接口级别、语言特性
    • 装饰器模式: 对象/接口级别、编程范式
  • 基本使用:
    @Aspect
    @Component
    public class MyAdvice {
        //对谁做增强
        @Pointcut("execution(void com.morningstar.dao.BookDao.save())")
        public void pt() {
        }
    
        //什么时候增强,增强哪些内容
        @After("pt()")
        public void after() {
            System.out.println("after");
        }
    }
  • 工作流程:
    • Spring容器启动
    • 读取所有切面配置中的切入点
    • 初始化bean,判定bean对应的类中的方法是否匹配到任意切入点
      • 匹配失败,创建对象
      • 匹配成功,创建原始对象(目标对象)的代理对象
    • 获取bean执行方法
      • 获取bean是非代理对象时,调用方法并执行
      • 获取的bean是代理对象时,根据代理对象的运行模式运行增强后的内容
  • 切入点表达式:
    • 标准格式: 动作关键字([访问修饰符] 返回值 包名.类/接口名.方法名(参数) [异常名])
    • 通配符:
      • *: 匹配任意符号
        execution(public * com.morningstar.*.UserService.find*(*))
        匹配com.morningstar包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法
      • ..: 匹配多个连续的任意符号【常用于简化包名与参数的书写】
        execution(public User com..UserService.findById(..))
        匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法
      • +: 专用于匹配子类类型
        execution(* *..*Service+.*(..))
    • 书写技巧: 【所有代码按照标准规范开发,否则以下技巧全部失效】
      • 描述切入点通常描述接口,而不描述实现类
      • 针对接口开发均采用public => 可省略访问控制修饰符描述
      • 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
      • 包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配
      • 接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名
      • 方法名书写以动词进行精准匹配,名词采用*匹配,例如getById书写成getBy*,selectAll书写成selectAll
      • 参数规则较为复杂,根据业务方法灵活调整
      • 通常不使用异常作为匹配规则
  • AOP通知
    @Aspect
    @Component
    public class MyAdvice {
        @Pointcut("execution(void com.morningstar.dao.BookDao.save())")
        public void pt() {
        }
        @Before("pt()")
        public void before() {
            System.out.println("before");
        }
        @After("pt()")
        public void after() {
            System.out.println("after");
        }
        @Around("pt()")
        public Object around(ProceedingJoinPoint pjp) throws Throwable {
            System.out.println("around before advice ...");
            Object ret = pjp.proceed();
            System.out.println("around after advice ...");
            return ret;
        }
        @AfterReturning("pt()")
        public void afterReturning() {
            System.out.println("afterReturning advice ...");
        }
        @AfterThrowing("pt()")
        public void afterThrowing() {
            System.out.println("afterThrowing advice ...");
        }
    }
    around before advice ...
    before
    book dao save ...
    afterReturning advice ...
    after
    around after advice ...
    • 前置通知
    • 后置通知
    • 环绕通知: 需要注意返回值与异常处理,抛出异常则运行后置代码
    • 返回后通知: 抛出异常则不运行
    • 抛出异常后通知: 无异常则不运行
  • 获取执行信息:
    • 信息参数
      • JoinPoint: 适用于前置、后置、返回后、抛出异常后通知
      • ProceedJointPoint: 适用于环绕通知【包含proceed()
    • 获取函数信息:
      • 获取函数所在类:
        • String joinPoint.getSignature().getDeclaringTypeName()
        • Class joinPoint.getSignature().getDeclaringType()
      • 获取函数名: joinPoint.getSignature().getName()
      • 判断修饰符: Modifier.isPublic(joinPoint.getSignature().getModifiers())
    • 获取切入点方法的参数: Object[] args = joinPoint.getArgs();
    • 获取切入点方法返回值:
      • 返回后通知:
        @AfterReturning(value = "pt()", returning = "ret")
        public void afterReturning(String ret) {
            System.out.println("afterReturning advice ..."+ret);
        }
      • 环绕通知: Object ret = pjp.proceed();
    • 获取切入点方法运行异常信息:
      • 抛出异常后通知:
        @AfterThrowing(value = "pt()", throwing = "t")
        public void afterThrowing(Throwable t) {
            System.out.println("afterThrowing advice ..."+ t);
        }
      • 环绕通知:
        @Around("pt()")
        public Object around(ProceedingJoinPoint pjp) {
            Object ret = null;
            try {
                ret = pjp.proceed();
            } catch (Throwable t) {
                t.printStackTrace();
            }
            return ret;
        }

典型案例

  • 测量接口万次执行效率
    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        Signature signature = pjp.getSignature();
        String className = signature.getDeclaringTypeName();
        String methodName = signature.getName();
        long start = System.currentTimeMillis();
        Object ret =null;
        for(int i=0; i< 10000;i++){
            ret = pjp.proceed();
        }
        long end = System.currentTimeMillis();
        System.out.println("万次执行:"+className+"."+methodName+"--->"+(end-start)+"ms");
        return ret;
    }
  • 密码空格兼容处理
    @Around("pt()")
    public Object trimString(ProceedingJoinPoint pjp) throws Throwable {
        Object[] args = pjp.getArgs();
        for (int i = 0; i < args.length; i++) {
            if(args[i].getClass().equals(String.class)){
                args[i] = args[i].toString().trim();
            }
        }
        return pjp.proceed(args);
    }

Spring事务

  • 概念:
    • 事务作用: 在数据层保障一系列的数据库操作同成功同失败
    • Spring事务作用: 在数据层或业务层保障一系列的数据库操作同成功同失败
  • 实现: 通过AOP,使用PlatformTransactionManager实现回滚与提交
  • 角色: 事务管理者(业务层方法)、事务协调员(数据层/业务层方法)
  • 使用:
    • SpringConfig上开启事务管理: @EnableTransactionManagement
    • 配置JDBC事务管理者
      @Configuration
      public class JdbcConfig {
          @Value("${jdbc.driver}")
          private String driver;
          @Value("${jdbc.url}")
          private String url;
          @Value("${jdbc.username}")
          private String name;
          @Value("${jdbc.password}")
          private String password;
      
          @Bean("dataSource")
          public DataSource getDataSource() {
              DruidDataSource druidDataSource = new DruidDataSource();
              druidDataSource.setDriverClassName(driver);
              druidDataSource.setUrl(url);
              druidDataSource.setUsername(name);
              druidDataSource.setPassword(password);
              return druidDataSource;
          }
      
          @Bean
          public PlatformTransactionManager transactionManager(DataSource dataSource){
              DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
              dataSourceTransactionManager.setDataSource(dataSource);
              return dataSourceTransactionManager;
          }
      }
      • 事务管理器要根据实现技术进行选择: MyBatis框架使用的是JDBC事务
    • 在业务层上启用事务
      public interface TransferService {
          @Transactional(rollbackFor = IOException.class)
          void transfer(String out, String in, Double money) throws IOException;
      }
      • Spring注解式事务通常添加在业务层接口中而不会添加到业务层实现类中,降低耦合
  • 事务属性配置:
    • readOnly: 设置是否为只读事务
      • readOnly=true: 只读事务
    • timeout: 设置事务超时时间
      • timeout = -1: 永不超时
    • rollbackFor: 设置事务回滚异常(class)
      • rollbackFor = {NullPointException.class}
    • rollbackForClassName: 设置事务回滚异常(String)
    • noRollbackFor: 设置事务不回滚异常(class)
      • noRollbackFor = {NullPointException.class}
    • noRollbackForClassName: 设置事务不回滚异常(String)
    • propagation: 设置事务传播行为(当事务方法被其他事务方法调用时,事务如何被传播)
      • Propagation.REQUIRED:这是最常见的传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务
      • Propagation.SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行
      • Propagation.MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常
      • Propagation.REQUIRES_NEW: 无论当前是否存在事务,都创建一个新的事务,并暂停当前事务(如果存在)
      • Propagation.NOT_SUPPORTED: 以非事务方式执行操作,如果当前存在事务,则暂停当前事务
      • Propagation.NEVER: 以非事务方式执行操作,如果当前存在事务,则抛出异常
      • Propagation.NESTED: 如果当前事务存在,则创建一个嵌套事务,并在嵌套事务内执行;如果当前没有事务,则创建一个新的事务【嵌套事务是同一个数据库连接上的保存点,可以独立于外部事务提交或回滚】

SpringMVC

基本概念

  • 定义: SpringMVC是一种基于Java实现MVC模型的轻量级web框架
  • 优点:
    • 使用简单,开发便捷(相比于ServletBaseServlet)
    • 灵活性强
  • 使用步骤:
    • 建项目并导包
      <packaging>war</packaging>
      <properties>
          <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
          <maven.compiler.source>1.8</maven.compiler.source>
          <maven.compiler.target>1.8</maven.compiler.target>
      </properties>
      <dependencies>
      <dependency>
          <groupId>javax.servlet</groupId>
          <artifactId>javax.servlet-api</artifactId>
          <version>3.1.0</version>
          <scope>provided</scope>
      </dependency>
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-webmvc</artifactId>
          <version>5.2.10.RELEASE</version>
      </dependency>
      </dependencies>
      <build>
          <plugins>
              <plugin>
                  <groupId>org.apache.tomcat.maven</groupId>
                  <artifactId>tomcat7-maven-plugin</artifactId>
                  <version>2.1</version>
                  <configuration>
                      <port>8080</port>
                      <path>/</path>
                  </configuration>
              </plugin>
          </plugins>
      </build>
    • 创建SpringMVC配置类
      @Configuration
      @ComponentScan("com.morningstar.controller")
      public class SpringMvcConfig {
      }
    • 创建Tomcat的Servlet容器配置类
      public class ServletContainerInitConfig extends AbstractDispatcherServletInitializer {
          //加载SpringMVC配置,创建SpringMVC容器
          protected WebApplicationContext createServletApplicationContext() {
              //初始化WebApplicationContext对象
              AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
              //加载指定配置类
              ctx.register(SpringMvcConfig.class);
              return ctx;
          }
          //设置Tomcat接收的请求哪些归SpringMVC(DispatcherServlet)处理
          protected String[] getServletMappings() {
              return new String[]{"/"};
          }
          //设置Spring相关配置,创建Spring容器
          protected WebApplicationContext createRootApplicationContext() {
              return null;
          }
      }
    • 创建控制器类
      @Controller
      @RequestMapping("user/")
      public class UserController {
          @RequestMapping(value = "save/", method = RequestMethod.POST)
          @ResponseBody
          public String save(){
              return "user save";
          }
      
          @RequestMapping(value = "",method = {RequestMethod.GET, RequestMethod.POST})
          @ResponseBody
          public String index(){
              return "user index";
          }
      }
    • 配置运行命令: tomcat7: run

请求参数解析

  • 解析方法:
    • Url传参(query_params)、表格传参(application/x-www-form-urlencoded)
      • 简单数据类型参数: String, Integer
        @RequestMapping("")
        @ResponseBody
        public String simpleParam(@RequestParam(name = "ageNum", required = false, defaultValue = "20") int age){
            return String.valueOf(age);
        }
      • pojo: 通过无参构造创建对应pojo,后通过set方法保存参数值【不支持改参数名】
        @RequestMapping("pojoParam/")
        @ResponseBody
        public String pojoParam(User user){
            System.out.println(user);
            return "pojo param";
        }
        • 嵌套pojo的传参: <url>?address.city=beijing
      • 数组:
        @RequestMapping("arrayParam/")
        @ResponseBody
        public String arrayParam(String[] likes){
            System.out.println(Arrays.toString(likes));
            return "array param";
        }
        • 传参方式: <url>?likes=game&likes=music
      • List:
        @RequestMapping("listParam/")
        @ResponseBody
        public String listParam(@RequestParam List<String> likes){
            System.out.println(likes);
            return "list param";
        }
        • 需要用@RequestParam将同名参数映射到对应名称的集合对象中,否则无法构建(没有无参构造)且无法传参(没有set方法)
      • Map:
        @RequestMapping("mapParam/")
        @ResponseBody
        public String mapParam(@RequestParam Map<String,String> maps) {
            System.out.println(maps);
            return "map Param";
        }
      • Date:
        @RequestMapping("dateParam/")
        @ResponseBody
        public String dateParam(Date date1, @DateTimeFormat(pattern = "yyyy-MM-dd") Date date2, @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date date3){
            System.out.println(date1);
            System.out.println(date2);
            System.out.println(date3);
            return "date param";
        }
        • 默认时间格式为yyyy/MM/dd
        • 其他时间格式需要使用@DateTimeFormat转换
        • 需要启用@EnableWebMvc注解来支持多种参数转换
    • File(multipart/form-data):
      • 添加commons-fileupload:commons-fileupload依赖
      • 配置multipartResolver解析器【Bean的名称是固定的,否则无法生效】
        @Bean("multipartResolver")
        public CommonsMultipartResolver multipartResolver (){
            CommonsMultipartResolver resolver = new CommonsMultipartResolver();
            resolver.setDefaultEncoding("UTF-8");
            resolver.setMaxUploadSize(1024*1024);
            return resolver;
        }
      • 后台接收参数
        @RequestMapping("fileParam/")
        @ResponseBody
        public String fileParam(MultipartFile file){
            if(!file.isEmpty()){
                try {
                    file.transferTo(new File("/Users/henry529/Downloads/test.txt"));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return "file param";
        }
    • JSON(application/json):
      • 添加com.fasterxml.jackson.core:jackson-databind依赖
      • SpringMvcConfig上启用@EnableWebMvc注解
      • 后台接收参数
        @RequestMapping("jsonParam/")
        @ResponseBody
        public String jsonParam(@RequestBody User user){
            System.out.println(user);
            return "json param";
        }
        • 使用@RequestBody支持json解析
        • 一个控制器方法中只能添加一个@RequestBody注解【JSON解析限制】
    • 路径参数:
      @RequestMapping("/users/{id}/")
      @ResponseBody
      public String pathParam(@PathVariable Integer id) {
          System.out.println(id);
          return "path param";
      }
  • 解决中文乱码:
    • 请求行参数:【一般不传送中文数据】
      <plugin>
          <groupId>org.apache.tomcat.maven</groupId>
          <artifactId>tomcat7-maven-plugin</artifactId>
          <version>2.2</version>
          <configuration>
              <port>8080</port>
              <path>/</path>
              <uriEncoding>UTF-8</uriEncoding>
          </configuration>
      </plugin>
    • 请求体: 通过ServletContainersInitConfig配置过滤器
      import javax.servlet.Filter;
      ...
      @Override
      protected Filter[] getServletFilters() {
          CharacterEncodingFilter filter = new CharacterEncodingFilter();
          filter.setEncoding("UTF-8");
          return new Filter[]{filter};
      }

响应体设置

  • 页面:
    @RequestMapping("/toJumpPage")
    public String toJumpPage(){
        return "/page.jsp";
    }
    • 返回对字符串不是/开头就会被解释成相对路径【基于RequestMapping最后一个/之前的路径】
  • 文本:
    @RequestMapping("/toText")
    @ResponseBody
    public String toText(){
        return "this is text";
    }
  • JSON:
    @RequestMapping("/toJson")
    @ResponseBody
    public User toJson(){
        User user = new User();
        user.setName("henry");
        return user;
    }
    • 需要启用@EnableWebMvc
  • ResponseEntity: 可以更灵活的控制响应
    @RequestMapping("/toEntity")
    public ResponseEntity<User> toEntity(){
        User user = new User();
        user.setName("henry");
        // return new ResponseEntity<>(user, HttpStatus.CREATED);;
        return ResponseEntity.ok()
                .headers(headers -> headers.add("X-Custom-Header", "CustomValue"))
                .body(user);
    }

类型转换接口

  • Converter: 简单类型转换
    /**
    *   S: the source type
    *   T: the target type
    */
    public interface Converter<S, T> {
        @Nullable
        //该方法就是将从页面上接收的数据(S)转换成我们想要的数据类型(T)返回
        T convert(S source);
    }
  • HttpMessageConverter: 用于转换HTTP请求和响应【实现了JSON、XML等数据类型的转换】

RESTful接口

@RestController //@Controller + ReponseBody
@RequestMapping("/books")
public class BookController {
    @PostMapping
    public String save(@RequestBody Book book){
        System.out.println("book save..." + book);
        return "{'module':'book save'}";
    }
    @DeleteMapping("/{id}")
    public String delete(@PathVariable Integer id){
        System.out.println("book delete..." + id);
        return "{'module':'book delete'}";
    }
    @PutMapping("/{id}")
    public String update(@PathVariable Integer id, @RequestBody Book book){
        System.out.println("book update..." + book);
        return "{'module':'book update'}";
    }
    @GetMapping("/{id}")
    public String getById(@PathVariable Integer id){
        System.out.println("book getById..." + id);
        return "{'module':'book getById'}";
    }
    @GetMapping
    public String getAll(){
        System.out.println("book getAll...");
        return "{'module':'book getAll'}";
    }
}

静态资源访问

  • 现象: 无法访问静态页面
  • 原因: ServletContainersInitConfig配置映射了根路径
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
  • 解决: 配置静态资源放行【继承WebMvcConfigurationSupport或者实现WebMvcConfigurer
    @Configuration
    public class SpringMvcSupport extends WebMvcConfigurationSupport {
        @Override
        protected void addResourceHandlers(ResourceHandlerRegistry registry) {
            //当访问/pages/????时候,从/pages目录下查找内容
            registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
            registry.addResourceHandler("/js/**").addResourceLocations("/js/");
            registry.addResourceHandler("/css/**").addResourceLocations("/css/");
            registry.addResourceHandler("/plugins/**").addResourceLocations("/plugins/");
        }
    }

运行原理

  • 技术架构:
  • 组件介绍:
    • DispatcherServlet: 前端控制器,是整体流程控制的中心,由其调用其它组件处理用户的请求,有效的降低了组件间的耦合性
    • HandlerMapping: 处理器映射器,负责根据用户请求找到对应具体的Handler
    • Handler: 处理器,业务处理的核心类,通常由开发者编写,描述具体的业务
    • HandlerAdapter: 处理器适配器,通过它执行处理器
    • View Resolver: 视图解析器,将处理结果生成View视图
    • View: 视图,最终产出结果,常用视图如jsp、html
  • 运行过程:
    • Servlet配置类ServletContainersInitConfig执行继承自AbstractDispatcherServletInitializerregisterDispatcherServlet方法注册DispatcherServlet
      • 通过SPI加载ServletContainerInitializer的实现类org. springframework.web.SpringServletContainerInitializer
      • 该实现类会查找系统中WebApplicationInitializer接口的实现类,并依次调用它们的onStartup方法
        @HandlesTypes(WebApplicationInitializer.class)
        public class SpringServletContainerInitializer implements ServletContainerInitializer{}
    • DispatcherServlet中的doDispatch先根据请求获取HandlerExecutionChain,再根据HandlerExecutionChain筛选适配的某个HandlerAdapter,通过这个HandlerAdapter执行处理器,最后处理执行结果ModelAndView
      ...
      HandlerExecutionChain mappedHandler = this.getHandler(HttpServletRequest processedRequest);
      ...
      HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
      ...
      ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
      ...
      this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
    • processDispatchResult判断ModelAndView是否为空
      if (mv != null && !mv.wasCleared()) {
          this.render(mv, request, response);
          ...
      }
      • 非空: 调用视图解析器viewResolver
      • 空: 不再调用视图解析器(加上@ResponseBody后不再走视图解析器)

SSM整合

需求:

  • 整合Spring、SpringMVC、Mybatis、Junit
  • 使用Result统一表现层响应结果: 将json响应体分为codemsgdata
  • 整合静态资源

代码:

  • pom.xml:
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>com.morningstar</groupId>
      <artifactId>ssm</artifactId>
      <packaging>war</packaging>
      <version>1.0-SNAPSHOT</version>
      <name>ssm Maven Webapp</name>
      <url>http://maven.apache.org</url>
      <dependencies>
        <!--  AOP  -->
        <dependency>
          <groupId>org.aspectj</groupId>
          <artifactId>aspectjweaver</artifactId>
          <version>1.9.7</version>
        </dependency>
        <!--  WEB  -->
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-webmvc</artifactId>
          <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
          <groupId>javax.servlet</groupId>
          <artifactId>javax.servlet-api</artifactId>
          <version>3.1.0</version>
          <scope>provided</scope>
        </dependency>
        <dependency>
          <groupId>com.fasterxml.jackson.core</groupId>
          <artifactId>jackson-databind</artifactId>
          <version>2.9.0</version>
        </dependency>
        <dependency>
          <groupId>commons-fileupload</groupId>
          <artifactId>commons-fileupload</artifactId>
          <version>1.3.3</version>
        </dependency>
        <!--  整合Junit  -->
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.13.2</version>
    <!--      <scope>test</scope>-->
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-test</artifactId>
          <version>5.2.10.RELEASE</version>
        </dependency>
        <!--  整合mybatis  -->
        <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>8.0.33</version>
          <scope>runtime</scope>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-jdbc</artifactId>
          <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>druid</artifactId>
          <version>1.2.23</version>
        </dependency>
        <dependency>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis</artifactId>
          <version>3.5.11</version>
        </dependency>
        <dependency>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis-spring</artifactId>
          <version>1.3.0</version>
        </dependency>
        <!--  日志  -->
    <!--    <dependency>-->
    <!--      <groupId>commons-logging</groupId>-->
    <!--      <artifactId>commons-logging</artifactId>-->
    <!--      <version>1.1.1</version>-->
    <!--    </dependency>-->
    <!--    <dependency>-->
    <!--      <groupId>log4j</groupId>-->
    <!--      <artifactId>log4j</artifactId>-->
    <!--      <version>1.2.17</version>-->
    <!--    </dependency>-->
        <!--  其他  -->
        <dependency>
          <groupId>commons-beanutils</groupId>
          <artifactId>commons-beanutils</artifactId>
          <version>1.9.4</version>
        </dependency>
      </dependencies>
      <build>
        <finalName>ssm</finalName>
        <plugins>
    <!--      <plugin>-->
    <!--        <groupId>org.apache.maven.plugins</groupId>-->
    <!--        <artifactId>maven-compiler-plugin</artifactId>-->
    <!--        <version>3.13.0</version>-->
    <!--        <configuration>-->
    <!--          &lt;!&ndash; put your configurations here &ndash;&gt;-->
    <!--          <source>1.8</source>-->
    <!--          <target>1.8</target>-->
    <!--          <encoding>UTF-8</encoding>-->
    <!--        </configuration>-->
    <!--      </plugin>-->
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-war-plugin</artifactId>
            <version>3.2.3</version>
            <configuration>
              <!--打包时忽略web.xml检查-->
              <failOnMissingWebXml>false</failOnMissingWebXml>
            </configuration>
          </plugin>
          <plugin>
            <groupId>org.apache.tomcat.maven</groupId>
            <artifactId>tomcat7-maven-plugin</artifactId>
            <version>2.2</version>
            <configuration>
              <port>8080</port>
              <path>/</path>
              <uriEncoding>UTF-8</uriEncoding>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </project>
  • resources/jdbc.properties:
    jdbc.driver=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3307/testdb?allowMultiQueries=true
    jdbc.username=root
    jdbc.password=1234asdw
  • com.morningstar.config.JdbcConfig:
    package com.morningstar.config;
    
    import com.alibaba.druid.pool.DruidDataSource;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.PlatformTransactionManager;
    import javax.sql.DataSource;
    
    public class JdbcConfig {
        @Value("${jdbc.driver}")
        private String driver;
        @Value("${jdbc.url}")
        private String url;
        @Value("${jdbc.username}")
        private String name;
        @Value("${jdbc.password}")
        private String password;
        @Bean("dataSource")
        public DataSource getDataSource() {
            DruidDataSource druidDataSource = new DruidDataSource();
            druidDataSource.setDriverClassName(driver);
            druidDataSource.setUrl(url);
            druidDataSource.setUsername(name);
            druidDataSource.setPassword(password);
            return druidDataSource;
        }
        @Bean
        public PlatformTransactionManager transactionManager(DataSource dataSource){
            DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
            dataSourceTransactionManager.setDataSource(dataSource);
            return dataSourceTransactionManager;
        }
    }
  • com.morningstar.config.MybatisConfig:
    package com.morningstar.config;
    
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.mapper.MapperScannerConfigurer;
    import org.springframework.context.annotation.Bean;
    import javax.sql.DataSource;
    
    public class MybatisConfig {
        @Bean
        public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setTypeAliasesPackage("com.morningstar.po");
            sqlSessionFactoryBean.setDataSource(dataSource);
            return sqlSessionFactoryBean;
        }
        @Bean
        public MapperScannerConfigurer mapperScannerConfigurer() {
            MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
        mapperScannerConfigurer.setBasePackage("com.morningstar.dao");
            return mapperScannerConfigurer;
        }
    }
  • com.morningstar.config.ServletContainerInitConfig:
    package com.morningstar.config;
    
    import org.springframework.web.filter.CharacterEncodingFilter;
    import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
    import javax.servlet.Filter;
    
    public class ServletContainerInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
        @Override
        protected Class<?>[] getRootConfigClasses() {
            return new Class[]{SpringConfig.class};
        }
        @Override
        protected Class<?>[] getServletConfigClasses() {
            return new Class[]{SpringMvcConfig.class};
        }
        @Override
        protected String[] getServletMappings() {
            return new String[]{"/"};
        }
        @Override
        protected Filter[] getServletFilters() {
            //乱码处理
            CharacterEncodingFilter filter = new CharacterEncodingFilter();
            filter.setEncoding("UTF-8");
            return new Filter[]{filter};
        }
    }
  • com.morningstar.config.SpringConfig:
    package com.morningstar.config;
    
    import org.springframework.context.annotation.*;
    import org.springframework.stereotype.Controller;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    
    @Configuration
    @ComponentScan({"com.morningstar.service", "com.morningstar.exception"})
    @Import({JdbcConfig.class, MybatisConfig.class})
    //@ComponentScan(value = "com.morningstar", excludeFilters = @ComponentScan.Filter(
    //                type = FilterType.ANNOTATION, classes = {Controller.class,Configuration.class}))
    @PropertySource("classpath:jdbc.properties")
    @EnableTransactionManagement
    public class SpringConfig {
    }
  • com.morningstar.config.SpringWebSupport:
    package com.morningstar.config;
    
    import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    public class SpringWebSupport implements WebMvcConfigurer {
        //设置静态资源访问路径
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
            registry.addResourceHandler("/css/**").addResourceLocations("/css/");
            registry.addResourceHandler("/js/**").addResourceLocations("/js/");
            registry.addResourceHandler("/plugins/**").addResourceLocations("/plugins/");
        }
    }
  • com.morningstar.config.SpringMvcConfig:
    package com.morningstar.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.multipart.commons.CommonsMultipartResolver;
    import org.springframework.web.servlet.config.annotation.EnableWebMvc;
    
    @Configuration
    @ComponentScan({"com.morningstar.controller","com.morningstar.interceptor"})
    @EnableWebMvc
    @Import(SpringWebSupport.class)
    public class SpringMvcConfig {
        @Bean("multipartResolver")
        public CommonsMultipartResolver multipartResolver (){
            CommonsMultipartResolver resolver = new CommonsMultipartResolver();
            resolver.setDefaultEncoding("UTF-8");
            resolver.setMaxUploadSize(1024*1024);
            return resolver;
        }
    }
  • tbl_book.sql:
    drop table if exists tbl_book;
    
    create table if not exists tbl_book (
        id INT PRIMARY KEY AUTO_INCREMENT,
        type VARCHAR(16),
        name VARCHAR(16),
        author VARCHAR(16)
    );
    show tables;
    
    insert into tbl_book (type, name, author) values ("传记", "史蒂夫·乔布斯传", "沃尔特·艾萨克森"), ("思维", "一只特立独行的猪", "王小波");
    
    select * from tbl_book;
  • com.morningstar.po.Book:
    package com.morningstar.po;
    
    public class Book {
        private Integer id;
        private String type;
        private String name;
        private String author;
        @Override
        public String toString() {
            return "Book{" +
                    "id=" + id +
                    ", type='" + type + '\'' +
                    ", name='" + name + '\'' +
                    ", author='" + author + '\'' +
                    '}';
        }
        public Integer getId() {
            return id;
        }
        public void setId(Integer id) {
            this.id = id;
        }
        public String getType() {
            return type;
        }
        public void setType(String type) {
            this.type = type;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public String getAuthor() {
            return author;
        }
        public void setAuthor(String author) {
            this.author = author;
        }
    }
  • com.morningstar.vo.Code:
    package com.morningstar.vo;
    
    public class Code {
        public static final Integer SAVE_OK = 20011;
        public static final Integer DELETE_OK = 20021;
        public static final Integer UPDATE_OK = 20031;
        public static final Integer SELECT_OK = 20041;
    
        public static final Integer SAVE_ERR = 20010;
        public static final Integer DELETE_ERR = 20020;
        public static final Integer UPDATE_ERR = 20030;
        public static final Integer SELECT_ERR = 20040;
    
        public static final Integer SYSTEM_TIME_OUT_ERR = 60010;
        public static final Integer SYSTEM_ERR = 60020;
        public static final Integer BUSINESS_ERR = 60030;
        public static final Integer ERROR = 00000;
    }
  • com.morningstar.vo.Result:
    package com.morningstar.vo;
    
    public class Result {
        //响应码
        private Integer code;
        //响应提示(可选)
        private String msg;
        //响应内容
        private Object data;
        public Result(Integer code, String msg, Object data) {
            this.code = code;
            this.msg = msg;
            this.data = data;
        }
        public Result(Integer code, Object data) {
            this.code = code;
            this.data = data;
        }
        @Override
        public String toString() {
            return "Result{" +
                    "code=" + code +
                    ", msg='" + msg + '\'' +
                    ", data=" + data +
                    '}';
        }
        public Integer getCode() {
            return code;
        }
        public void setCode(Integer code) {
            this.code = code;
        }
        public String getMsg() {
            return msg;
        }
        public void setMsg(String msg) {
            this.msg = msg;
        }
        public Object getData() {
            return data;
        }
        public void setData(Object data) {
            this.data = data;
        }
    }
  • com.morningstar.dao.BookDao:
    package com.morningstar.dao;
    
    import com.morningstar.po.Book;
    import org.apache.ibatis.annotations.Insert;
    import org.apache.ibatis.annotations.Select;
    import java.util.List;
    
    public interface BookDao {
        @Insert("insert into tbl_book (type,name,author) values(#{type},#{name},#{author})")
        Integer save(Book book);
        @Select("select * from tbl_book")
        List<Book> getAll();
        @Select("select * from tbl_book where id = #{id}")
        Book getById(Integer id);
    }
  • com.morningstar.service.BookService:
    package com.morningstar.service;
    
    import com.morningstar.po.Book;
    import org.springframework.transaction.annotation.Transactional;
    import java.util.List;
    
    @Transactional
    public interface BookService {
        //插入操作
        Boolean save(Book book);
        //查询全部
        List<Book> getAll();
        //根据ID查询
        Book getById(Integer id);
    }
  • com.morningstar.service.BookServiceImpl:
    package com.morningstar.service;
    
    import com.morningstar.dao.BookDao;
    import com.morningstar.po.Book;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import java.util.List;
    
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    @Service
    public class BookServiceImpl implements BookService {
        @Autowired
        private BookDao bookDao;
        public Boolean save(Book book) {
            Integer save = bookDao.save(book);
            return save > 0;
        }
        public List<Book> getAll() {
            return bookDao.getAll();
        }
        public Book getById(Integer id) {
            return bookDao.getById(id);
        }
    }
  • com.morningstar.service.BookServiceTest:
    package com.morningstar.service;
    
    import com.morningstar.config.SpringConfig;
    import com.morningstar.po.Book;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    import org.junit.runner.RunWith;
    import org.junit.Test;
    import java.util.List;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = SpringConfig.class)
    public class BookServiceTest {
        @Autowired
        private BookService bookService;
        @Test
        public void testGetAll(){
            List<Book> bookList = bookService.getAll();
            System.out.println(bookList);
        }
        @Test
        public void testGetById(){
            Book book = bookService.getById(3);
            System.out.println(book);
        }
        @Test
        public void testSave(){
            Book book = new Book();
            book.setName("三体");
            book.setType("科幻");
            book.setAuthor("刘慈欣");
            Boolean save = bookService.save(book);
            System.out.println(save);
        }
    }
  • com.morningstar.controller.BookController:
    package com.morningstar.controller;
    
    import com.morningstar.po.Book;
    import com.morningstar.service.BookService;
    import com.morningstar.vo.Code;
    import com.morningstar.vo.Result;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;
    import java.util.List;
    
    @RestController
    @RequestMapping("/books")
    public class BookController {
        @Autowired
        private BookService bookService;
        //插入操作
        @PostMapping
        public Result save(@RequestBody Book book) {
            Boolean result = bookService.save(book);
            Integer code = result? Code.SAVE_OK:Code.SAVE_ERR;
            String msg = result ? "插入成功" : "插入失败";
            return new Result(code, msg, result);
        }
        //查询全部
        @GetMapping
        public Result getAll() {
            List<Book> bookList = bookService.getAll();
            Integer code = bookList!=null?Code.SELECT_OK:Code.SELECT_ERR;
            String msg = bookList!=null? "查找成功" : "查找失败";
            return new Result(code, msg, bookList);
        }
        //根据ID查询
        @GetMapping("/{id}")
        public Result getById(@PathVariable Integer id) {
            Book book = bookService.getById(id);
            Integer code = book != null ? Code.SELECT_OK : Code.SELECT_ERR;
            String msg = book != null ? "查找成功" : "查找失败";
            return new Result(code, msg, book);
        }
    }

异常处理

  • 异常分类:
    • 按位置:
      • 框架内部抛出的异常: 因使用不合规导致
      • 数据层抛出的异常: 因外部服务器故障导致【服务器访问超时】
      • 业务层抛出的异常: 因业务逻辑书写错误导致【索引异常】
      • 表现层抛出的异常: 因数据收集、校验等规则导致【不匹配的数据类型间】
      • 工具类抛出的异常: 因工具类书写不严谨不够健壮导致【必要释放的连接长期未释放】
    • 按来源:
      • 业务异常(BusinessException)
      • 规范的用户行为产生的异常【表单填写错误,但前端没检查就传到了后端】
      • 不规范的用户行为操作产生的异常【用户在地址栏瞎填】
      • 系统异常(SystemException)
      • 项目运行过程中可预计且无法避免的异常【网络错误、硬件故障、数据库挂掉、第三方服务宕机】
      • 其他异常(Exception)
      • 编程人员未预期到的异常【编程逻辑错误】
  • 异常处理:
    • 表现层处理异常: 每个方法中单独书写,代码书写量巨大且意义不强
    • AOP思想解决: @RestControllerAdvice + @ExceptionHandler
  • 典型方案:
    • 业务异常(BusinessException)
    • 发送对应消息传递给用户,提醒规范操作
    • 系统异常(SystemException)
    • 发送固定消息传递给用户,安抚用户
    • 发送特定消息给运维人员,提醒维护
    • 记录日志
    • 其他异常(Exception)
    • 发送固定消息传递给用户,安抚用户
    • 发送特定消息给编程人员,提醒维护(纳入预期范围内)
    • 记录日志
  • 基本流程:
    • 自定义异常编码与异常类
      package com.morningstar.exception;
      
      public class BusinessException extends RuntimeException{
          private Integer code;
      
          public BusinessException(Integer code,String message) {
              super(message);
              this.code = code;
          }
          public BusinessException(Integer code,String message, Throwable cause) {
              super(message, cause);
              this.code = code;
          }
          public Integer getCode() {
              return code;
          }
          public void setCode(Integer code) {
              this.code = code;
          }
      }
      package com.morningstar.exception;
      
      public class SystemException extends RuntimeException{
          private Integer code;
          public SystemException(Integer code, String message) {
              super(message);
              this.code = code;
          }
          public SystemException(Integer code, String message, Throwable cause) {
              super(message, cause);
              this.code = code;
          }
          public Integer getCode() {
              return code;
          }
          public void setCode(Integer code) {
              this.code = code;
          }
      }
    • 在异常通知类中拦截并处理异常
      package com.morningstar.exception;
      
      import com.morningstar.vo.Code;
      import com.morningstar.vo.Result;
      import org.springframework.web.bind.annotation.ExceptionHandler;
      import org.springframework.web.bind.annotation.RestControllerAdvice;
      
      @RestControllerAdvice  //用于标识当前类为REST风格对应的异常处理器
      public class ProjectExceptionAdvice {
          @ExceptionHandler(SystemException.class)
          public Result doSystemException(SystemException e) {
              return new Result(e.getCode(), e.getMessage(), null);
          }
          @ExceptionHandler(BusinessException.class)
          public Result doBussinessException(BusinessException e) {
              return new Result(e.getCode(), e.getMessage(), null);
          }
          @ExceptionHandler(Exception.class)
          public Result doOtherException(Exception ex){
              return new Result(Code.ERROR,"项目出错了",null);
          }
      }

拦截器

  • 定义: (Interceptor) 一种动态拦截方法调用的机制,在SpringMVC中动态拦截控制器方法的执行
  • 作用:
  • 在指定的方法调用前后执行预先设定的代码
  • 阻止原始方法的执行
  • 原理: AOP
  • 流程:
    • 单拦截器
    • 拦截器链
  • 对比过滤器:
    • 归属不同: Filter属于Servlet技术,Interceptor属于SpringMVC技术
    • 拦截内容不同: Filter对所有访问进行增强,Interceptor仅针对SpringMVC的访问进行增强
  • 基本使用:
    package com.morningstar.interceptor;
    
    import org.springframework.stereotype.Component;
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    @Component
    public class ProjectInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            System.out.println("preHandle...");
            System.out.println(handler);
            System.out.println(handler.getClass());
            return true;
        }
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            System.out.println("postHandle...");
        }
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            System.out.println("afterCompletion...");
        }
    }
    package com.morningstar.config;
    
    import com.morningstar.interceptor.ProjectInterceptor;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    public class SpringWebSupport implements WebMvcConfigurer {
        @Autowired
        private ProjectInterceptor projectInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(projectInterceptor).addPathPatterns("/books","/books/*/");
        }
    }
  • 函数说明:
    • handler: (HandlerMethod) 对Method对象的包装
      • Controller的某个方法
    • preHandle:
      • 原始方法调用前执行
    • postHandle:
      • 原始方法调用后,JSP渲染前
      • 如果处理器方法出现异常了,该方法不会执行
      • 对响应进行加解密处理、编码处理
    • afterCompletion:
      • JSP渲染后
      • 无论处理器方法内部是否出现异常,该方法都会执行
      • 可以捕捉异常,但一般不在这里捕捉

Spring Boot

  • 作用: 简化Spring应用的初始搭建以及开发过程【pom依赖导入、配置文件编写】
  • 功能:
    • 创建独立的 Spring 应用程序【不需要导入第三方包】
    • 提供"启动器"依赖项以简化构建配置
    • 直接嵌入Tomcat、Jetty 或 Undertow【无需部署 WAR 文件】
    • 开箱即用,自动配置Spring和第三方库,不需要代码生成,也不需要XML配置
    • 提供可用于生产的功能,例如指标、健康检查和外部化配置
  • 版本:
    • SpringBoot2.x基于Spring5.x,SpringBoot3.x基于Spring6.x
    • SpringBoot3.x要求最低使用Java 17
    • SpringBoot3.x已从Java EE迁移到Jakarta EE API,导致很多依赖需要修改【如swagger】

启用MVC

  • 创建SpringMVC项目【也可以通过Idea借助Spring Initializr创建】
    • pom.xml
      <?xml version="1.0" encoding="UTF-8"?>
      <project xmlns="http://maven.apache.org/POM/4.0.0"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>
          <parent>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-parent</artifactId>
              <version>2.3.10.RELEASE</version>
          </parent>
          <groupId>com.morningstar</groupId>
          <artifactId>demo</artifactId>
          <version>1.0-SNAPSHOT</version>
          <dependencies>
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-web</artifactId>
              </dependency>
          </dependencies>
      
          <properties>
              <maven.compiler.source>8</maven.compiler.source>
              <maven.compiler.target>8</maven.compiler.target>
              <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
          </properties>
      
          <build>
              <plugins>
                  <plugin>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-maven-plugin</artifactId>
                  </plugin>
              </plugins>
          </build>
      </project>
    • com.morningstar.Application
      package com.morningstar;
      
      import org.springframework.boot.SpringApplication;
      import org.springframework.boot.autoconfigure.SpringBootApplication;
      
      @SpringBootApplication
      public class Application {
          public static void main(String[] args) {
              SpringApplication.run(Application.class, args);
          }
      }
      • @SpringBootApplication只会加载同级与次级的Bean
    • com.morningstar.controller.BookController
      package com.morningstar.controller;
      
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.PathVariable;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RestController;
      
      @RestController
      @RequestMapping("/books")
      public class BookController {
          @GetMapping("/{id}")
          public String getById(@PathVariable Integer id) {
              System.out.println("id ==> " + id);
              return "hello , spring boot! ";
          }
      }
  • 打包Spring Boot项目(形成jar包)
    • 需要spring-boot-maven-plugin依赖,会打出两个包: 带.original后缀的是原始的jar包;另一个是可以运行的jar包,包含BOOT-INF目录来存放业务代码和依赖,通过org.springframework.boot.loader.JarLauncher来启动
  • 运行springboot的jar包

基础配置

  • 配置优先级: application.properties > application.yml > application.yaml
  • 配置读取:
    • 使用@Value读取单个数据
      @Value("${student.name}")
      private String name;
    • 使用Environment对象读取
      @Autowired
      private final Environment environment;
      ...
      System.out.println(environment.getProperty("student.name"));
    • 自定义对象封装指定数据【常用】
      • 编写与固定引入
        package com.morningstar.config;
        
        import org.springframework.boot.context.properties.ConfigurationProperties;
        import org.springframework.stereotype.Component;
        import java.util.Arrays;
        
        @Component
        @ConfigurationProperties(prefix = "student")
        public class Student {
            private String name;
            private int age;
            private String[] wives;
        
            @Override
            public String toString() {
                return "Student{" +
                        "name='" + name + '\'' +
                        ", age=" + age +
                        ", wives=" + Arrays.toString(wives) +
                        '}';
            }
        }
      • 根据配置类按需引入
        @EnableConfigurationProperties(Student.class)
        @Configuration
        public class CommonConfig {
            //省略N行
        }

多环境配置

  • 纯SpringBoot配置
    • 配置方式
      • yaml配置
        • application.yml
          spring:
            profiles:
              active: test
        • application-dev.yml
          server:
            port: 8080
        • application-test.yml
          server:
            port: 8081
        • applocation-prod.yml
          server:
            port: 8082
      • properties配置
        • application.properties
          spring.profiles.active=prod
        • application-dev.properties
          server.port=8080
        • application-test.properties
          server.port=8081
        • application-prod.properties
          server.port=8082
    • 带参启动:
      java –jar springboot.jar --server.port=88 --spring.profiles.active=test
    • 配置bean的加载条件:
      @Bean
      @Profile("dev")
      CommandLineRunner init(DevDataInitializer devDataInitializer) {
          return args -> {
              devDataInitializer.initializeData(); // 调用数据初始化
          };
      }
  • Maven+SpringBoot配置
    • 配置方法
      • pom.xml
        <profiles>
            <profile>
                <id>dev_env</id>
                <properties>
                    <profile.active>dev</profile.active>
                </properties>
                <activation>
                    <activeByDefault>true</activeByDefault>
                </activation>
            </profile>
            <profile>
                <id>prod_env</id>
                <properties>
                    <profile.active>prod</profile.active>
                </properties>
            </profile>
            <profile>
                <id>test_env</id>
                <properties>
                    <profile.active>test</profile.active>
                </properties>
            </profile>
        </profiles>
      • application.properties
        spring.profiles.active=@profile.active@
        • 不使用${}是因为继承的maven-resources-plugin插件配置
        • 可以修改回${}
          <build>
              <plugins>
                  <plugin>
                      <artifactId>maven-resources-plugin</artifactId>
                      <configuration>
                          <encoding>utf-8</encoding>
                          <useDefaultDelimiters>true</useDefaultDelimiters>
                      </configuration>
                  </plugin>
              </plugins>
          </build>
  • 配置文件等级: 1级与2级留做打包后设置通用属性,3级与4级用于开发中设置通用属性
    • 1级: file:config/application.yml 【最高】
    • 2级: file:application.yml
    • 3级: classpath:config/application.yml
    • 4级: classpath:application.yml 【最低】

SSM整合

  • 整合Mybatis
    • 通过Spring Initializr创建模版【需要添加Mybatis和MySQL依赖】
    • 引入连接池依赖
          <dependency>
              <groupId>com.alibaba</groupId>
              <artifactId>druid</artifactId>
              <version>1.2.23</version>
          </dependency>
    • 配置application.yml
      spring:
        datasource:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3307/testdb?allowMultiQueries=true
          username: root
          password: 1234asdw
          type: com.alibaba.druid.pool.DruidDataSource
    • 编写Mapper(需要添加@Mapper以取代MapperScannerConfigurer)
  • 整合junit
    • 配置依赖
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-test</artifactId>
          <scope>test</scope>
      </dependency>
    • 编写测试类
      package com.morningstar.dao;
      
      import com.morningstar.Demo1Application;
      import org.junit.jupiter.api.Test;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
      
      @SpringBootTest(classes = Application.class)
      public class BookDaoTest {
      
          @Autowired
          BookDao bookDao;
      
          @Test
          public void test(){
              System.out.println(bookDao.getById(1));
          }
      }
      • @SpringBootTest可以指定引导类【默认需要在Application的同级或者下一级路径】
  • 整合页面: 放入resource/static

自动化配置原理

starter依赖管理机制

  • 所有(无论是否为官方的)的starter都依赖spring-boot-starter
  • 官方starter都被spring-boot-dependencies管理了版本

SpringMVC自动化配置原理

2.3.10.RELEASE版本为例:

spring-boot-autoconfigure-2.3.10.RELEASE中的META-INF/spring.factories定义了一系列接口的实现类。其中,EnableAutoConfiguration负责自动化配置。

  • 三大组件的配置: WebMvcAutoConfiguration
    • HandlerMapping配置
          @Bean
          @Primary
          @Override
          public RequestMappingHandlerMapping requestMappingHandlerMapping(
                  @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
                  @Qualifier("mvcConversionService") FormattingConversionService conversionService,
                  @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {
              // Must be @Primary for MvcUriComponentsBuilder to work
              return super.requestMappingHandlerMapping(contentNegotiationManager, conversionService,
                      resourceUrlProvider);
          }
    • HandlerAdapter配置
          @Bean
          @Override
          public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
                  @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
                  @Qualifier("mvcConversionService") FormattingConversionService conversionService,
                  @Qualifier("mvcValidator") Validator validator) {
              RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter(contentNegotiationManager,
                      conversionService, validator);
              adapter.setIgnoreDefaultModelOnRedirect(
                      this.mvcProperties == null || this.mvcProperties.isIgnoreDefaultModelOnRedirect());
              return adapter;
          }
    • ViewResolver配置
          @Bean
          @ConditionalOnBean(View.class)
          @ConditionalOnMissingBean
          public BeanNameViewResolver beanNameViewResolver() {
              BeanNameViewResolver resolver = new BeanNameViewResolver();
              resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
              return resolver;
          }
  • 文件上传配置: MultipartAutoConfiguration
    @Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
    @ConditionalOnMissingBean(MultipartResolver.class)
    public StandardServletMultipartResolver multipartResolver() {
        StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
        multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
        return multipartResolver;
    }
  • 字符编码配置: HttpEncodingAutoConfiguration
    public HttpEncodingAutoConfiguration(ServerProperties properties) {
        this.properties = properties.getServlet().getEncoding();
    }
    • ServerProperties.ServletgetEncoding()获取的Encoding.DEFAULT_CHARSETStandardCharsets.UTF_8
  • 静态资源访问:
    • 根据经验,由WebMvcConfigurationSupport中的protected void addResourceHandlers(ResourceHandlerRegistry registry)接口负责
    • WebMvcAutoConfiguration覆盖了这一配置【@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
          @Override
          protected void addResourceHandlers(ResourceHandlerRegistry registry) {
              super.addResourceHandlers(registry);
              if (!this.resourceProperties.isAddMappings()) {
                  logger.debug("Default resource handling disabled");
                  return;
              }
              addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
              addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(),
                      this.resourceProperties.getStaticLocations());
      
          }
  • 服务器端口配置: ServerProperties
    @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
    public class ServerProperties {
    
        /**
         * Server HTTP port.
         */
        private Integer port;
        ...
    }

@Configuration

  • 作用: 替代原始Spring配置文件功能
  • 源码:
    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Component
    public @interface Configuration {
        @AliasFor(
            annotation = Component.class
        )
        String value() default "";
    
        boolean proxyBeanMethods() default true;
    
        boolean enforceUniqueMethods() default true;
    }
    • proxyBeanMethods(): 为true说明启用了Full模式(单例模式),否则为Lite模式
      • 组件依赖必须使用Full模式默认
      • Full模式每次都会检查bean,效率较Lite模式慢
  • 案例: 以下代码输出false
    @Configuration(proxyBeanMethods = false)
    public class MyConfig {
        @Bean("book")
        public Book getBook() {
            return new Book();
        }
    }
    @SpringBootApplication
    public class Demo1Application {
        public static void main(String[] args) {
            ConfigurableApplicationContext ctx = SpringApplication.run(Demo1Application.class, args);
            System.out.println(ctx.getBean("myConfig", MyConfig.class).getBook() == ctx.getBean("book"));
        }
    }

@Import

  • 作用: 使用@Import导入的类会被Spring加载到IoC容器中
  • 用法:
    • 导入自身:
      @SpringBootApplication
      @Import(Book.class)
      public class Demo1Application {
          public static void main(String[] args) {
              ConfigurableApplicationContext ctx = SpringApplication.run(Demo1Application.class, args);
              System.out.println(ctx.getBean("com.morningstar.po.Book"));
          }
      }
      • bean的名称为全路径
      • 一般用于导入配置类,等同于添加@Configuration注解
    • 辅助导入别的Bean: 【不导入自身】
      • 导入ImportSelector实现类【一般用于加载配置文件中的类(spring.factories)】
        public class MyImportSelector implements ImportSelector {
            @Override
            public String[] selectImports(AnnotationMetadata importingClassMetadata) {
                return new String[]{"com.morningstar.po.Book"};
            }
        }
      • 导入ImportBeanDefinitionRegistrar实现类:
        public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
            @Override
            public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
                AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder
                        .rootBeanDefinition(Book.class).getBeanDefinition();
                registry.registerBeanDefinition("book", beanDefinition);
            }
        }
        • BeanDefinition是Bean的元数据
      • 区别:
        • 功能范围: ImportBeanDefinitionRegistrar可以注册新的Bean 或 修改已有的BeanDefinition,而ImportSelector主要用于选择性地导入配置类
        • 使用场景: 如果你需要在容器启动时动态注册Bean,或者需要根据条件判断注册哪些Bean,使用ImportBeanDefinitionRegistrar更合适;如果你需要根据条件选择性地导入配置类,使用ImportSelector更合适

@ConditionalOnXXX

  • 作用: 满足条件当前类或者Bean才有效,按需导入
  • 分类:
    • @ConditionalOnBean(Class<?>[]): 存在Bean则导入
    • @ConditionalOnMissingBean(Class<?>[]):
    • @ConditionalOnClass: 存在类则导入
    • @ConditionalOnMissingClass
  • 案例: 以下代码会加载BookBean
    @Configuration
    @ConditionalOnMissingBean(Book.class)
    public class MyConfig {
        @Bean("book")
        public Book getBook() {
            return new Book();
        }
    }

@ConfigurationProperties

  • 作用: 获取指定前缀的配置属性,并且初始化Bean对象到 IOC 容器。
  • 定义: 【ServerProperties的源码】
    @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
    public class ServerProperties {
        /**
         * Server HTTP port.
         */
        private Integer port;
        ...
    }
  • 使用:
    • @Component
    • @EnableConfigurationProperties【在配置类上使用】
    • @Bean

@SpringBootApplication

  • 作用: 配置SpringBoot的启动引导类
  • 源码:
    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @SpringBootConfiguration
    @EnableAutoConfiguration
    @ComponentScan(
        excludeFilters = {@Filter(
        type = FilterType.CUSTOM,
        classes = {TypeExcludeFilter.class}
    ), @Filter(
        type = FilterType.CUSTOM,
        classes = {AutoConfigurationExcludeFilter.class}
    )}
    )
    public @interface SpringBootApplication {
        ...
    • @SpringBootConfiguration: 本质上就是一个Configuration注解
      @Target({ElementType.TYPE})
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      @Configuration
      @Indexed
      public @interface SpringBootConfiguration {
          @AliasFor(
              annotation = Configuration.class
          )
          boolean proxyBeanMethods() default true;
      }
      • 意味着,在引导类中配置@Bean注解也可以
    • @ComponentScan: 默认扫描注解添加位置所在包及子包中所有带@Component注解的类

@EnableAutoConfiguration

以3.2.7的源码为例

  • 作用: 实现第三方包的自动化配置
  • 源码:
    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @AutoConfigurationPackage
    @Import({AutoConfigurationImportSelector.class})
    public @interface EnableAutoConfiguration {
        String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    
        Class<?>[] exclude() default {};
    
        String[] excludeName() default {};
    }
    • @AutoConfigurationPackage: 导入了AutoConfigurationPackages.Registrar
      @Target({ElementType.TYPE})
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      @Inherited
      @Import({AutoConfigurationPackages.Registrar.class})
      public @interface AutoConfigurationPackage {
          String[] basePackages() default {};
      
          Class<?>[] basePackageClasses() default {};
      }
      • 主要作用是导入AutoConfigurationPackages.Registrar,默认情况下会将引导类的所有包及其子包的组件导入进来
        static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
            ...        
            public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
                AutoConfigurationPackages.register(registry, (String[])(new PackageImports(metadata)).getPackageNames().toArray(new String[0]));
            }
            ...
        }
        • new PackageImports(metadata)).getPackageNames().toArray(new String[0])) 返回字符数组形式的注册包路径,默认为引导类所在的包路径
          private static final class PackageImports {
              private final List<String> packageNames;
          
              PackageImports(AnnotationMetadata metadata) {
                  AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(AutoConfigurationPackage.class.getName(), false));
                  List<String> packageNames = new ArrayList(Arrays.asList(attributes.getStringArray("basePackages")));
                  ...
              }
          
              List<String> getPackageNames() {
                  return this.packageNames;
              }
          }
    • 导入了AutoConfigurationImportSelector,通过selectImports获取getCandidateConfigurations读取的默认配置类
      protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
          List<String> configurations = ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).getCandidates();
          ...
          return configurations;
      }
      protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
          ...
          AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
          List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
          configurations = this.removeDuplicates(configurations);
          Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
          this.checkExcludedClasses(configurations, exclusions);
          configurations.removeAll(exclusions);
          configurations = this.getConfigurationClassFilter().filter(configurations);
          this.fireAutoConfigurationImportEvents(configurations, exclusions);
          return new AutoConfigurationEntry(configurations, exclusions);
          }
      }
      • getAutoConfigurationEntry通过ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).getCandidates()读取META-INF/spring/%s.imports(org.springframework.boot.autoconfigure.AutoConfiguration.imports)中的配置信息
        org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration
        org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration
        org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
      • 按需加载配置: 这些配置类并不会都加载,而是要通过大量的@ConditionalOnXXX选择性的加载,一般都依赖于所需的类和配置文件【因此,starter不能随便导入】

自动化配置原理总结

  • 自动化配置流程总结:
    • 程序启动找到自动化配置包下META-INF/spring/%s.imports
    • 加载所有的自动配置类
    • 每个自动配置类按照条件进行生效
    • 生效的配置类就会给容器中装配很多组件
    • 只要容器中有这些组件,相当于这些功能就有了
    • 定制化配置
    • 用户直接自己@Bean替换底层的组件
    • 用户去看这个组件是获取的配置文件什么值就去修改
  • 开发人员使用步骤总结:
    • 引入场景依赖
    • 查看自动配置了哪些(选做)【配置文件中debug=true开启自动配置报告】
    • 修改配置或替换组件

健康监控

健康监控数据服务

  • 依赖: org.springframework.boot:spring-boot-starter-actuator
  • 作用: 为每个云上部署的微服务提供监控、追踪、审计、控制
  • 使用:
    • 导入依赖
    • 基本配置
      management:
        endpoints:
          enabled-by-default: true # 暴露所有端点信息
          web:
            exposure:
              include: '*'
        endpoint:
          health:
            enabled: true # 开启健康检查详细信息
            show-details: always
    • 访问主路径: http://localhost:8080/actuator

健康监控数据可视化

  • 依赖: de.codecentric:spring-boot-admin-starter-server
  • 功能: 为注册的应用程序提供以下功能【甚至支持python应用】
    • 显示健康状况
    • 显示详细信息,例如
    • JVM和内存指标
    • micrometer.io指标
    • 数据源指标
    • 缓存指标
    • 显示内部信息
    • 关注并下载日志文件
    • 查看JVM系统和环境属性
    • 查看Spring Boot配置属性
    • 轻松的日志级别管理
    • 与JMX-beans交互
    • 查看线程转储
    • 查看http-traces
    • 查看审核事件
    • 查看http端点
    • 查看预定的任务
    • 查看和删除活动会话(使用spring-session)
    • 查看Flyway/Liquibase数据库迁移
    • 下载heapdump
    • 状态更改通知(通过电子邮件,Slack,Hipchat等)
    • 状态更改的事件日志(非持久性)
  • 文档: 快速入门
  • 使用: 分别搭建客户端和服务端【注意版本问题】
    • 搭建服务端
      <dependency>
          <groupId>de.codecentric</groupId>
          <artifactId>spring-boot-admin-starter-server</artifactId>
          <version>3.3.2</version>
      </dependency>
      import de.codecentric.boot.admin.server.config.EnableAdminServer;
      import org.springframework.boot.SpringApplication;
      import org.springframework.boot.autoconfigure.SpringBootApplication;
      
      @SpringBootApplication
      @EnableAdminServer
      public class AdminApplication {
          public static void main(String[] args) {
              SpringApplication.run(AdminApplication.class, args);
          }
      }
      • 注意修改端口: server.port = 9999
    • 搭建客户端
      <dependency>
          <groupId>de.codecentric</groupId>
          <artifactId>spring-boot-admin-starter-client</artifactId>
          <version>3.3.2</version>
      </dependency>
      spring:   
        boot:
          admin:
            client:
              url: http://localhost:9999 # admin 服务地址
              instance:
                prefer-ip: true # 显示IP
        application:
          name: boot_data # 项目名称
      
      management:
        endpoints:
          enabled-by-default: true # 暴露所有端点信息
          web:
            exposure:
              include: '*' # 以web方式暴露所有可用的端点【生产环境不推荐】
        endpoint:
          health:
            enabled: true # 开启健康检查详细信息
            show-details: always # 其他可选值: `when_authorized`和`never`

常见问题

WebMvcConfigurationSupport与WebMvcConfigurer

  • 参考文章: https://developer.aliyun.com/article/1233256
  • 为何选用WebMvcConfigurer:
    • WebMvcConfigurationSupport中那些子类可以重写的空方法在WebMvcConfigurer都有,说明WebMvcConfigurer只是WebMvcConfigurationSupport的一个扩展类,它并没有扩展新功能
    • 可以为让用户更方便安全的添加自定义配置,避免用户不小心重写了默认的配置
    • 只有当WebMvcConfigurationSupport类不存在的时候才会生效WebMvc自动化配置

序列化配置

  • 局部配置:
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonSerialize(using = SeasonStatsSerializer.class)
    @JsonInclude(JsonInclude.Include.NON_NULL)
  • 全局配置
    • 配置类
      protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
          log.info("扩展消息转换器...");
          //创建一个消息转换器对象
          MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
          //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
          converter.setObjectMapper(new JacksonObjectMapper());
          //将自己的消息转化器加入容器中
          converters.add(0,converter);
      }
      public class JacksonObjectMapper extends ObjectMapper {
      
          public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
          //public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
          public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
          public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
      
          public JacksonObjectMapper() {
              super();
              //收到未知属性时不报异常
              this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
      
              //反序列化时,属性不存在的兼容处理
              this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
      
              SimpleModule simpleModule = new SimpleModule()
                      .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                      .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                      .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
                      .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                      .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                      .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
      
              //注册功能模块 例如,可以添加自定义序列化器和反序列化器
              this.registerModule(simpleModule);
          }
      }
    • 配置文件
      spring:
          jackson:
              date-format: "yyyy-MM-dd HH:mm:ss"
              default-property-inclusion: non_null # serialization-inclusion=non_null

项目部署

  • jar包部署【通常推荐】
    • 步骤:
      • 引入打包插件依赖
        <build>
          <!--jar 包名称-->
          <finalName>boot_data</finalName>
          <plugins>
            <plugin>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
          </plugins>
        </build>
      • 运行程序
        nohup java -jar XXX.jar &
    • 优势:
      • 独立运行: Spring Boot内嵌了Tomcat、Jetty或Undertow等Servlet容器
      • 简化部署: 由于不需要WAR文件和额外的Servlet容器,部署变得更加简单快捷,只需一个JAR文件
      • 体积更小: JAR文件通常比WAR文件更小,因为它们不包含Servlet容器的库,这些库在WAR文件中是必需的,但在JAR中已经被内嵌容器所包含
      • 便于微服务架构: JAR包部署可以更好地适应微服务,因为它们可以快速启动和停止,便于容器化和规模化
      • 持续集成/持续部署(CI/CD): 现代的CI/CD流程通常更适合处理JAR文件,因为它们简单且易于管理
  • war包部署
    • 步骤:
      • 更改打包方式为war
      • 配置打包插件
      • 修改引导类: 继承SpringBootServletInitializer
      • 配置tomcat,war复制到webapps目录下
      • 启动tomcat
    • 优势:
      • 特殊场景: 需要将Spring Boot应用部署到一个外部的Servlet容器中,或者你的应用需要与容器中的其他WAR应用集成【越来越少】

数据访问

Spring Data Redis

基本使用

  • 介绍: Spring Data Redis中提供RedisTemplate,将同一类型操作封装为Operation接口【通用命令可以直接通过RedisTemplate调用】
    • ValueOperations: 简单K-V操作(String类型)
    • SetOperations
    • ZSetOperations
    • HashOperations
    • ListOperations
  • 获取:
    • 直接根据Maven坐标
      <dependency>
          <groupId>org.springframework.data</groupId>
          <artifactId>spring-data-redis</artifactId>
          <version>2.4.8</version>
      </dependency>
    • 通过starter坐标【推荐】
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
      </dependency>
  • 使用:
    • 配置application.yml:
      spring:
        data:
          redis:
            host: 127.0.0.1
            port: 6379
            database: 0
            password: 1234asdw
    • 自动装配:
      @Autowired
      private RedisTemplate redisTemplate;
    • 基本操作:
          ValueOperations valueOperations = redisTemplate.opsForValue();
          valueOperations.set("city123","beijing");
      
          String value = (String) valueOperations.get("city123");
          System.out.println(value);

Json序列化配置

package com.morningstar.config;

import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());

        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }
}
- 这样的配置只会修改key,value还是序列化的【方便高级对象的存储】 - 默认序列化器为JdkSerializationRedisSerializer

连接池配置

  • 导入依赖
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
  • 配置application.yml
    spring:
      data:
        redis:
          host: 127.0.0.1
          port: 6379
          database: 0
          password: 1234asdw
          lettuce:
            pool:
              max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
              max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
              max-idle: 8 # 连接池中的最大空闲连接
              min-idle: 1  # 连接池中的最小空闲连接

事务操作

  • 开启事务控制
    redisTemplate.setEnableTransactionSupport(true);
  • 测试事务效果
    @Test
    public void multiTest(){
        //开启事务
        redisTemplate.multi();
        try{
            redisTemplate.opsForValue().set("lesson","java");
            redisTemplate.opsForSet().add("lesson","eureka","feign","gateway");
            redisTemplate.opsForValue().set("lesson","redis");
            System.out.println(redisTemplate.opsForValue().get("lesson"));
        }catch (Exception e){
            //回滚
            System.out.println("出现异常");
            redisTemplate.discard();
        }finally {
            redisTemplate.exec();
        }
    }

集群连接配置

spring:
  redis:
    cluster:
      nodes:
        - 192.168.200.130:7001
        - 192.168.200.130:7002
        - 192.168.200.130:7003
        - 192.168.200.130:7004
        - 192.168.200.130:7005
        - 192.168.200.130:7006

服务异步通讯

基础知识

  • 通讯方式对比
    • 同步通讯:
      • 优点:
        • 简单直观,容易理解、实现和调试
        • 确保数据一致性
        • 实时性较好
        • 易于实现事务
      • 缺点:
        • 耦合度高
        • 性能下降
        • 资源浪费
        • 级联失败
    • 同步通讯: 事件驱动模式
      • 优点:
        • 性能(吞吐量)提升: 无需等待订阅者处理完成,响应更快速
        • 故障隔离: 服务没有直接调用,不存在级联失败问题
        • 降低耦合: 每个服务都可以灵活插拔
        • 流量削峰: 不管发布事件的流量波动多大,都由Broker接收,订阅者可以按照自己的速度去处理事件
      • 缺点:
        • 架构复杂了,业务没有明显的流程线,难以追踪管理
        • 依赖Broker的可靠、安全、性能
  • 异步技术对比: 消息队列(MessageQueue, MQ)就是事件驱动架构中的Broker
    • ActiveMQ: 支持AMQP,XMPP,SMTP,STOMP协议 | 可用性高 | 单机吞吐量一般 | 消息延迟微秒级 | 消息可靠性高 | 社区不活跃
    • RabbitMQ: 支持OpenWire,STOMP,REST,XMPP,AMQP协议 | 可用性一般 | 单机吞吐量差 | 消息延迟毫秒级 | 消息可靠性一般 | 开发语言门槛影响原理掌握
    • RocketMQ: 使用自定义协议 | 可用性高 | 单机吞吐量高 | 消息延迟毫秒级 | 消息可靠性高 | 社区不活跃
    • Kafka: 使用自定义协议 | 可用性高 | 单机吞吐量非常高 | 消息延迟毫秒以内 | 消息可靠性一般 | 大数据领域的业内标准
  • 技术选择:
    • 追求可用性(当需要处理数据时,资源处于可用状态的程度): Kafka、 RocketMQ 、RabbitMQ
    • 追求可靠性: RabbitMQ、RocketMQ
    • 追求吞吐能力(十万级别的): RocketMQ、Kafka
    • 追求消息低延迟: RabbitMQ、Kafka

RabbitMQ

  • 定位: 消息队列中间件(消息代理软件)
  • 作用:
    • 实现异步处理
    • 解耦应用程序组件
    • 提高系统可用性和扩展性
  • 版本: 分为基础版和管理版(包含Web管理界面)
  • 流程:
    • Message(消息): RabbitMQ中的基本数据单元,由消息头和消息体组成【消息体是不透明的数据,而消息头则包含了一些元数据】
    • Publisher(生产者/发布者): 发送消息的应用程序或服务,将消息发送到RabbitMQ服务器上的一个交换机(Exchange)
    • Consumer(消费者): 接收消息的应用程序或服务,从RabbitMQ服务器上的一个队列(Queue)中接收消息
    • Exchange(交换机): 负责将消息路由到一个或多个队列
    • Queue(队列): 负责存储消息
    • VirtualHost(虚拟主机): 隔离不同用户的exchange、queue、message
    • Channel(信道): 是消息发布者和交换机之间的连接通道,也是消息消费者连接队列的通道【基于物理连接(Connection)的逻辑连接】
    • Acknowledgment(确认): 消费者在成功处理消息后,会向RabbitMQ发送一个确认信号,以确保消息在处理失败时不会丢失
  • RabbitMQ消息模型
    • 基本消息队列(Basic Queue)
      • P(producer/publisher): 生产者,发送消息的用户应用程序
      • C(consumer): 消费者,等待接收消息的用户应用程序
      • 队列: 存储消息的缓冲区
    • 工作消息队列(Work Queue/Task Queue): 使用多个消费者来消费队列中的消息【效率要比基本消息队列模型高】
    • 发布订阅(Pub/Sub): 生产者没有将消息直接发送到队列,而是发送到了交换机【可对比观察者模式、Guava EventBus
      • Fanout Exchange(广播): 生产者发布消息,所有消费者都可以获取所有消息
      • Direct Exchange(路由): 队列与交换机的绑定需要RoutingKey,消息发送方也需要RoutingKey
      • Headers Exchange(属性匹配): 允许消息队列系统根据消息的多个属性来决定消息的路由
        • 作用: 如果消息的 headers 中包含某队列中所有匹配模式中指定的键,并且对应的值也相等,则消息会被路由到该队列
        • 举例:
          • 消息具有以下headers: {"user_id": 123, "message_type": "alert"}
          • 队列A绑定到headers交换机,匹配模式为{"user_id": 123}
          • 队列B绑定到同一个headers交换机,匹配模式为{"message_type": "alert"}
          • 在这种情况下,如果消息到达交换机,它将同时被路由到队列A和队列 B,因为消息的headers同时满足了两个队列的匹配模式
        • 特点: 提供了比direct更复杂的路由能力,但可能在性能上不如direct 交换机,因为它需要进行更多的属性比较
      • Topic Exchange(主题): 绑定RoutingKey的时候使用通配符
        • RoutingKey一般都是有一个或多个单词组成,多个单词之间以”.”分割(e.g. item.insert)
          • #: 匹配一个或多个词
          • *: 匹配恰好一个词
        • 接收的消息RoutingKey必须是多个单词,以.分割
    • 远程过程调用协议(Remote Procedure Call, RPC): 允许程序调用另一台计算机上的程序或服务上的方法,就像调用本地程序一样简单的技术
      • RPC协议旨在简化跨网络的服务调用过程
      • 使得开发者在使用远程服务时,不必关心底层的网络通信细节,而是能够像调用本机上的方法一样方便
      • 远程方法调用(Remote Method Invocation,RMI): 计算机之间利用远程对象互相调用实现双方通讯的一种通讯机制
        • 地位: Enterprise JavaBeans的支柱,是建立分布式Java应用程序的方便途径
        • 特点: RPC未能做到面向对象调用的开发模式,而RMI允许程序员使用远程对象来实现通信,并且支持多线程的服务
        • 协议: 默认采用的是TCP/IP
  • 具体实现:【简单队列模式】
    • publisher: 建立connection -> 创建channel -> 声明队列 -> 向队列发送消息 -> 关闭connection和channel
      import com.rabbitmq.client.Channel;
      import com.rabbitmq.client.Connection;
      import com.rabbitmq.client.ConnectionFactory;
      
      // 1. 建立连接
      ConnectionFactory factory = new ConnectionFactory();
      factory.setHost("127.0.0.1");
      factory.setPort(5672);
      factory.setVirtualHost("/");
      factory.setUsername("henry");
      factory.setPassword("1234asdw");
      Connection connection = factory.newConnection();
      // 2. 创建通道Channel
      Channel channel = connection.createChannel();
      // 3. 创建队列
         /*
          声明队列
              参数1: 队列的名称
              参数2: 队列是否支持持久化 false:不持久化处理
              参数3: 队列是否排它(是否允许其它connection下的channel连接)
              参数4: 是否空闲时自动删除
       */
      String queueName = "simple.queue";
      channel.queueDeclare(queueName, false, false, false, null);
      // 4.发送消息
      String message = "hello, rabbitmq!";
      channel.basicPublish("", queueName, null, message.getBytes());
      System.out.println("发送消息成功:【" + message + "】");
      // 5.关闭通道和连接
      channel.close();
      connection.close();
    • consumer: 建立connection -> 创建channel -> 声明队列 -> 订阅消息(需定义消费行为)
      import com.rabbitmq.client.*;
      
      // 1. 建立连接
      ConnectionFactory factory = new ConnectionFactory();
      factory.setHost("127.0.0.1");
      factory.setPort(5672);
      factory.setVirtualHost("/");
      factory.setUsername("henry");
      factory.setPassword("1234asdw");
      Connection connection = factory.newConnection();
      // 2. 创建通道Channel
      Channel channel = connection.createChannel();
      // 3. 创建队列
      String queueName = "simple.queue";
      channel.queueDeclare(queueName, false, false, false, null);
      // 4. 订阅消息
         /*
          参数1: 消费者消费的队列名称
          参数2: 收到消息后自动应答,通知rabbitmq自动剔除已经被消费的消息
          参数3: 接口消息的回调(一旦队列下有新的消息,则自动回调DefaultConsumer对象下的handleDelivery方法)把消息以入参传入到该方法中
       */
      channel.basicConsume(queueName, true, new DefaultConsumer(channel){
          @Override
          public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
              // 5.处理消息
              String message = new String(body);
              System.out.println("接收到消息:【" + message + "】");
          }
      });
      System.out.println("等待接收消息。。。。");
  • RabbitMQ中合法的目的前缀: /temp-queue, /exchange, /topic, /queue, /amq/queue, /reply-queue/
    • /exchange/<exchangeName>[/pattern]
    • /queue/<queueName>: 不应用于群发,应用于"负载均衡"
    • /amq/queue/<queueName>: 需要自己创建queue
    • /topic/<topicName>

Spring AMQP

基本概念

  • 高级消息队列协议: (Advanced Message Queuing Protocol, AMPQ) 是用于在应用程序之间传递业务消息的开放标准。该协议与语言和平台无关,更符合微服务中独立性的要求。
  • SpringAMQP: 基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息
    • 自动声明队列、交换机及其绑定关系
    • 基于注解的监听器模式,异步接收消息
    • 封装了Template工具,用于发送和接收消息
  • SpringRabbit: SpringAMQP底层的默认实现【提供类似RedisTemplateRabbitTemplate

环境配置

  • 配置依赖与连接
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    spring:
      rabbitmq:
        host: 127.0.0.1  # 主机名
        port: 5672 # 发送消息和接收消息的端口号
        virtual-host: / # 虚拟主机
        username: henry # 用户名
        password: 1234asdw # 密码
  • 创建所需队列
    • 在Web界面创建所需队列
    • @RabbitListener(queuesToDeclare=@Queue("simple.queue"))创建

Work Queue

  • Publisher
    @SpringBootTest
    class PublisherApplicationTests {
        @Test
        void contextLoads() {
        }
        @Autowired
        private RabbitTemplate rabbitTemplate;
        @Test
        public void testWorkQueue() throws InterruptedException {
            // 队列名称
            String queueName = "simple.queue";
            // 消息
            String message = "hello, message_";
            for (int i = 0; i < 50; i++) {
                // 发送消息
                rabbitTemplate.convertAndSend(queueName, message + i);
                Thread.sleep(20);
            }
        }
    }
  • Consumer【一般先运行】
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.stereotype.Component;
    
    @Component
    public class SpringRabbitListener {
        @RabbitListener(queues = "simple.queue")
        public void listenWorkQueue1(String msg) throws InterruptedException {
            System.out.println("消费者1接收到消息:【" + msg + "】" + new Date());
            Thread.sleep(20);
        }
        @RabbitListener(queues = "simple.queue")
        public void listenWorkQueue2(String msg) throws InterruptedException {
            System.err.println("消费者2........接收到消息:【" + msg + "】" + new Date());
            Thread.sleep(100);
        }
    }
  • 配置预取【能者多劳】
    spring:
      rabbitmq:
        listener:
          simple:
            prefetch: 1 # 消费者每次最多只能预取一条消息

Fanout

  • Consumer:
    • 声明队列和交换机
      import org.springframework.amqp.core.Binding;
      import org.springframework.amqp.core.BindingBuilder;
      import org.springframework.amqp.core.FanoutExchange;
      import org.springframework.amqp.core.Queue;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      
      @Configuration
      public class FanoutConfig {
          @Bean
          public FanoutExchange fanoutExchange(){
              return new FanoutExchange("morningstar.fanout");
          }
      
          @Bean
          public Queue fanoutQueue1(){
              return new Queue("fanout.queue1");
          }
      
          @Bean
          public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
              return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
          }
      
          @Bean
          public Queue fanoutQueue2(){
              return new Queue("fanout.queue2");
          }
      
          @Bean
          public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
              return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
          }
      }
    • 设置消息接收
      @RabbitListener(queues = "fanout.queue1")
      public void listenFanoutQueue1(String msg) {
          System.out.println("消费者1接收到Fanout消息:【" + msg + "】");
      }
      
      @RabbitListener(queues = "fanout.queue2")
      public void listenFanoutQueue2(String msg) {
          System.out.println("消费者2接收到Fanout消息:【" + msg + "】");
      }
  • 客户端
    • 消息发送
      @Test
      public void testFanoutExchange() {
          String exchangeName = "itcast.fanout";
          String message = "hello, everyone!";
          rabbitTemplate.convertAndSend(exchangeName, "", message);
      }

Direct

  • Consumer
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue1"),
            exchange = @Exchange(name = "morningstar.direct", type = ExchangeTypes.DIRECT), //这里换成ExchangeTypes.TOPIC就可以使用支持通配符的topic模式
            key = {"red", "blue"}
    ))
    public void listenDirectQueue1(String msg){
        System.out.println("消费者接收到direct.queue1的消息:【" + msg + "】");
    }
    
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue2"),
            exchange = @Exchange(name = "morningstar.direct", type = ExchangeTypes.DIRECT),
            key = {"red", "yellow"}
    ))
    public void listenDirectQueue2(String msg){
        System.out.println("消费者接收到direct.queue2的消息:【" + msg + "】");
    }
  • Publisher
    @Test
    public void testSendDirectExchange() {
        // 交换机名称
        String exchangeName = "itcast.direct";
        // 消息
        String message = "红色警报!日本乱排核废水,导致海洋生物变异,惊现哥斯拉!";
        // 发送消息
        rabbitTemplate.convertAndSend(exchangeName, "red", message);
    }

消息转换器

  • Spring会Publisher发送的消息序列化为字节发送给MQ,还会把接收到的消息字节反序列化为Java对象
  • JDK序列化存在下列问题:
    • 数据体积过大
    • 有安全漏洞
    • 可读性差
  • 配置JSON转换器
    • 添加依赖
      <dependency>
          <groupId>com.fasterxml.jackson.dataformat</groupId>
          <artifactId>jackson-dataformat-xml</artifactId>
          <version>2.9.10</version>
      </dependency>
    • 引入转换器
      @Configuration
      public class MessageConverterConfig{
          @Bean
          public MessageConverter jsonMessageConverter(){
              return new Jackson2JsonMessageConverter();
          }
      }

缓存系统

CaffineCache

认证与授权

认证与授权

基本概念

  • 表设计: 经典权限五张表
    • 权限 -> 职责【隐含资源信息】
    • 用户 -> 职员
    • 角色 -> 职位
  • 权限模型
    • RBAC(Role-Based Access Control):
      • 缺点: 根据角色判断权限容易产生许多修改,不满足开闭原则
    • RBAC(Resource-Based Access Control):
      • 优点: 资源操作标签不容易变动,这样不需要改源码,只需修改数据库
  • 实现:
    • Apache Shiro: Apache旗下的一款安全框架
    • SpringSecurity: Spring家族的一部分, Spring体系中提供的安全框架, 包含认证、授权两个大的部分
    • CAS: CAS是一个单点登录(SSO)服务,开始是由耶鲁大学的一个组织开发,后来归到apereo去管
    • 自行实现: 自行通过业务代码实现(基于filter过滤器或者springmvc拦截器+AOP), 实现繁琐, 代码量大

JWT(JSON Web Token)

常见认证方式: https://morningstar369.com/posts/33/

  • 简介: JSON Web Token(JWT)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。该token被设计为紧凑且安全的,特别适用于前后端无状态认证的场景。
  • 原理: 以下三个部分用.连接成一个完整的字符串,构成最终的JWT
    • 头部(Header)(非敏感): 用于描述关于该JWT的最基本的信息,例如数据类型以及签名所用的算法等【e.g. {"typ": "JWT", "alg": "HS256"}
    • 载荷(playload)(非敏感): 存放有效信息的地方,该部分的信息是可以自定义的【{"sub": "1234567890", "name": "John Doe", "admin": true}
    • 签证(signature): 签名算法(header[base64后的].payload[base64后的].secret)
  • 评价: 推荐阅读一文带你搞懂JWT常见概念 &优缺点
    • 优点:
      • 使用json作为数据传输,有广泛的通用型,并且体积小,便于传输
      • 不需要在服务器端保存相关信息,节省内存资源的开销
      • jwt载荷部分可以存储业务相关的信息(非敏感的),例如用户信息、角色等
      • 如果使用非对称加密,可以用公钥来验证签名,适合大型/分布式系统
    • 缺点:
      • 注销登录等场景下JWT还有效
        • 退出登录
        • 修改密码
        • 服务端修改了某个用户具有的权限或者角色
        • 用户的帐户被封禁/删除
        • 用户被服务端强制注销
        • 用户被踢下线
      • JWT的续签问题
    • 总结:
      • JWT其中一个很重要的优势是无状态,但实际上,我们想要在实际项目中合理使用JWT的话,也还是需要保存JWT信息
      • JWT也不是银弹,也有很多缺陷,具体是选择JWT还是Session方案还是要看项目的具体需求
      • 不用JWT直接使用普通的Token(随机生成,不包含具体的信息)结合Redis来做身份认证也是可以的
  • 实验: 使用io.jsonwebtoken:jjwt
    @Test
    public void testGenerate(){
        String compact = Jwts.builder()
            .setId(UUID.randomUUID().toString())//设置唯一标识
            .setSubject("JRZS") //设置主题
            .claim("name", "nineclock") //自定义信息
            .claim("age", 88) //自定义信息
            .setExpiration(new Date()) //设置过期时间
            .setIssuedAt(new Date()) //令牌签发时间
            .signWith(SignatureAlgorithm.HS256, "morningstar")//签名算法, 秘钥
            .compact();
        System.out.println(compact);
    }
    @Test
    public void testVerify(){
        String jwt = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI5MzljNjU4MC0yMTQyLTRlOWEtYjcxOC0yNzlmNzRhODVmNDMiLCJzdWIiOiJOSU5FQ0xPQ0siLCJuYW1lIjoibmluZWNsb2NrIiwiYWdlIjo4OCwiaWF0IjoxNjE3MDMxMjUxfQ.J-4kjEgyn-Gkh0ZuivUCevrzDXt0K9bAyF76rn1BfUs";
        Claims claims = Jwts.parser().setSigningKey("itheima").parseClaimsJws(jwt).getBody();
        System.out.println(claims);
    }

Spring Security

基本概念

  • 简介:
    • Spring Security是基于Spring的安全框架,提供了包含认证和授权的落地方案;
    • Spring Security底层充分利用了Spring IOC和AOP功能,为企业应用系统提供了声明式安全访问控制解决方案;
    • SpringSecurity可在Web请求级别和方法调用级别处理身份认证和授权,为应用系统提供声明式的安全访问控制功能;
  • 原理: (Servlet Filter + AOP) 当初始化Spring Security时,会创建SpringSecurityFilterChain,外部的请求会经过该类
    • SecurityContextPersistenceFilter: 整个拦截过程的入口和出口,
      • 在请求进入应用程序时从SecurityContextRepository中获取 SecurityContext
      • 将获取的SecurityContext绑定到当前线程的 SecurityContextHolder
      • 在请求处理完成后,将SecurityContextSecurityContextHolder中取出,并存储到SecurityContextRepository中,同时清除securityContextHolder所持有的SecurityContext
        • 在存储SecurityContext的过程中,会确保JSESSIONID Cookie被正确地设置
        • 在存储SecurityContext的过程中,通过HttpSessionSecurityContextRepository类将SecurityContext数据存储到HTTP Session中,并在此过程中设置 JSESSIONID Cookie
    • 原生的认证过滤器(UsernamePasswordAuthenticationFilter)用于处理来自表单提交的认证,该表单必须提供usernamepassword,其内部还有登录成功或失败后进行处理的AuthenticationSuccessHandlerAuthenticationFailureHandler,处理流程如下
      • 认证过滤器接收form表单提交的账户、密码信息,并封装成UsernamePasswordAuthenticationToken认证凭据对象
      • 认证过滤器调用认证管理器AuthenticationManager进行认证处理
      • 认证管理器通过调用UserDetailsService获取用户详情UserDetails
      • 认证管理器通过密码匹配器PasswordEncoder进行匹配,如果密码一致,则将用户相关的权限信息一并封装到Authentication认证对象中【存入principal属性】
      • 认证过滤器将Authentication认证对象放到安全上下文SecurityContextHolder(基于ThreadLocal),方便请求从上下文获取认证信息
    • ExceptionTranslationFilter能够捕获来自FilterChain所有的异常,并进行处理。但是它只会处理两类异常: AuthenticationExceptionAccessDeniedException,其它的异常它会继续抛出
    • FilterSecurityInterceptor是用于权限校验的,会从SecurityContextHolder中获取Authentication对象,然后获取其中的权限信息,再用AccessDecisionManager对当前用户进行授权访问

基于JWT的认证与授权

  • 构建:
    • 登录:
      • 实现UserDetailsService接口中的loadUserByUsername(String username)方法
      • 组装Authentication对象,通过AuthenticationManager进行认证,成功后将用户信息放入redis,失败则抛出认证异常
      • 响应中返回jwt
    • 认证:
      • 自定义认证过滤器,解析请求头中的jwt
      • 根据jwt中的userId找到redis中对应的LoginUser对象
      • loginUser作为principal装入authentication并存入安全上下文
      • 将该过滤器设置在默认认证过滤器前
    • 授权:
      • userDetailsService中获取权限列表存入UserDetails对象(loginUser)
      • 在认证过程中,将loginUser中的权限信息作为authorities装入authentication
    • 登出:
      • 根据请求中的token通过认证过滤器,获取安全上下文中的Authentication对象
      • 找到redis中对应的用户信息对象并删除,从而让token失效
  • 配置:
    • 集中配置: 基于配置类
      ...
      .antMatchers("/hello").hasAuthority("P5") //具有P5权限才可以访问
      .antMatchers("/say").hasRole("ADMIN") //具有ROLE_ADMIN角色才可以访问
      .anyRequest().authenticated(); //除了上边配置的请求资源,其它资源都必须授权才能访问
      ...
    • 分布式配置: 基于注解
      @PreAuthorize("hasAuthority('P5')")
      @GetMapping("/hello")
      public String hello(){
          return "hello security";
      }
      @PreAuthorize("hasRole('ADMIN')") // 不推荐使用
      @GetMapping("/say")
      public String say(){
          return "say security";
      }
      @PermitAll
      //@PreAuthorize("isAnonymous()")
      @GetMapping("/register")
      public String register(){
          return "register security";
      }
  • 自定义权限拒绝处理
    • 自定义认证用户权限拒绝处理器【实现AccessDeniedHandler接口】
    • 自定义匿名用户拒绝处理器【实现AuthenticationEntryPoint接口】

自定义权限校验方法

  • 定义:
    @Component("ex")
    public class SGExpressionRoot {
    
        public boolean hasAuthority(String authority){
            //获取当前用户的权限
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            LoginUser loginUser = (LoginUser) authentication.getPrincipal();
            List<String> permissions = loginUser.getPermissions();
            //判断用户权限集合中是否存在authority
            return permissions.contains(authority);
        }
    }
  • 调用:
        @RequestMapping("/hello")
        @PreAuthorize("@ex.hasAuthority('system:dept:list')")
        public String hello(){
            return "hello";
        }

WebSocket

原生Websocket

  • 基本原理:
    • 相关header:
      • Connection: upgrade
      • Upgrade: websocket
      • Sec-WebSocket-Extensions: permessage-deflate;Client_max_window_bits=15: 允许压缩单个消息以减少传输数据的大小,指定了压缩算法的窗口大小
      • Sec-WebSocket-Key/Sec-WebSocket-Accept: 主要作用在于提供基础的防护,减少恶意连接、意外连接
    • 特性:
      • 优点: 全双工、长连接
      • 缺点: 连接维持成本、重连等问题
    • 应用: 实时游戏、网页聊天、体育实况、股票行情、视频弹幕、实时更新
  • 基本使用
    • 客户端
      var wsObj = null;
      var wsUri = null;
      var userId = -1;
      var lockReconnect = false; //避免重复连接
      var wsCreateHandler = null;
      
      function GetQueryString(name) {
          var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
          var r = window.location.search.substr(1).match(reg); //获取url中"?"符后的字符串并正则匹配
          var context = "";
          if (r != null)
              context = r[2];
          reg = null;
          r = null;
          return context == null || context == "" || context == "undefined" ? "" : context;
      }
      
      function createWebSocket() {
          var host = window.location.host; // 带有端口号
          userId = GetQueryString("userId");
          wsUri = "ws://" + host + "/websocket/" + userId;
          try {
              wsObj = new WebSocket(wsUri);
              initWsEventHandle();
          } catch (e) {
              writeToScreen("执行关闭事件,开始重连");
              reconnect();
          }
      }
      
      function initWsEventHandle() {
          try{
              wsObj.onopen = function (evt) {
                  heartCheck.start();
                  onWsOpen(evt);
              };
              wsObj.onmessage = function (evt) {
                  heartCheck.start();
                  onWsMessage(evt);
              };
              wsObj.onclose = function (evt) {
                  writeToScreen("执行关闭事件,开始重连");
                  onWsClose(evt);
                  reconnect();
              };
              wsObj.onerror = function (evt) {
                  writeToScreen("执行error事件,开始重连");
                  onWsError(evt);
                  reconnect();
              };
          } catch (e) {
              writeToScreen("绑定事件没有成功");
              reconnect();
          }
      }
      
      function reconnect() {
          if(lockReconnect) {
              return;
          };
          writeToScreen("1秒后重连");
          lockReconnect = true;
          // 没连接上会一直重连,设置延迟避免请求过多
          wsCreateHandler && clearTimeout(wsCreateHandler);
          wsCreateHandler = setTimeout(function () {
              writeToScreen("重连..." + wsUri);
              createWebSocket();
              lockReconnect = false;
              writeToScreen("重连完成");
          }, 1000);
      }
      
      var heartCheck = {
          // 15s之内如果没有收到后台的消息,则认为是连接断开了,需要再次连接
          timeout: 15000,
          timeoutObj: null,
          serverTimeoutObj: null,
             // 重启
          reset: function(){
              clearTimeout(this.timeoutObj);
              clearTimeout(this.serverTimeoutObj);
              this.start();
          },
          // 开启定时器
             start: function(){
                 var self = this;
                 this.timeoutObj && clearTimeout(this.timeoutObj);
                 this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
                 this.timeoutObj = setTimeout(function(){
                     writeToScreen("发送ping到后台");
                     try{
                         wsObj.send("ping");
                     }
                     catch(ee){
                         writeToScreen("发送ping异常");
                     }
                     //内嵌计时器
                     self.serverTimeoutObj = setTimeout(function(){
                         // writeToScreen("没有收到后台的数据,关闭连接");
                         // wsObj.close();
                         reconnect();
                     }, self.timeout);
                 }, this.timeout)
             },
      }
      
      wsObj.send("hello");
      wsObj.send(JSON.stringify(data));
      • WS对象相关事件
        • open(ws.onopen): 连接建立时触发
        • message(ws.onmessage): 客户端接收到服务器发送的数据时触发
        • close(ws.onclose): 连接关闭时触发
        • error(ws.onerror): 连接出错时触发
      • 相关方法:
        • send(): 通过websocket对象调用该方法发送数据给服务端
      • 异常处理: 连接关闭或出错后设置定时重连【避免过多重连】
      • 心跳检测: 在onopenonmessage的方法末尾调用heartCheck.start()
    • 服务端
      @Configuration
      public class WebSocketConfig {
          @Bean
          public ServerEndpointExporter serverEndpointExporter() {
              // 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
              return new ServerEndpointExporter();
          }
      }
      public class SessionPool {
          public static Map<String, Session> sessions = new ConcurrentHashMap<String, Session>();
      
          public static void close(String sessionId) throws IOException {
              for(String userId : SessionPool.sessions.keySet()){
                    Session session = SessionPool.sessions.get(userId);
                    if(session.getId().equals(sessionId)){
                        sessions.remove(userId);
                        break;
                    }
                }
          }
      
          public static void sendMessage(String message) {
              for(String userId : sessions.keySet()){
                  sessions.get(userId).getAsyncRemote().sendText(message);
              }
          }
      
          public static void sendMessage(String userId, String message) {
              Session session = sessions.get(userId);
              if(session != null && session.isOpen()){
                  session.getAsyncRemote().sendText(message);
              }
          }
      
          public static void sendMessage(Map<String, Object> params){
              // {"fromUserId": userId,"toUserId": toUserId,"msg": msg};
              String msg = params.get("msg").toString();
              String fromUserId = params.get("fromUserId").toString();               
              String toUserId = params.get("toUserId").toString();
              msg = String.format("来自用户%s发送给用户%s,消息:%s",  fromUserId, toUserId, msg);
              sendMessage(toUserId, msg);
          }
      }
      @ServerEndpoint(value = "/websocket/{userId}")
      @Component
      public class WebSocketEndpoint {
          private Session session;
      
          @OnOpen
          public void onOpen(Session session, @PathParam("userId") String userId){                    
                  this.session = session;
                  SessionPool.sessions.put(userId, session);
          }
      
          @OnClose
          public void onClose(Session session) {
              SessionPool.close(session.getId());
              session.close();
          }
      
          @OnMessage
          public void onMessage(String message, Session session) {
              // System.out.println("来自客户端的消息:" + message);
              Map<String, Object> params = null;
              if(message.equalsIgnoreCase("ping")){
                  params = new HashMap<String, Object>();
                  params.put("type", "pong");
              }else{
                  params = JSON.parseObject(message, params.getClass());
              }
              // System.out.println("应答客户端的消息:" + JSON.toJSONString(params));
              SessionPool.sendMessage(params);
          }
      }
      • API:
        • 版本: Tomcat的7.0.5版本开始支持WebSocket,并且实现了Java WebSocket规范
        • 依赖: org.springframework.boot:spring-boot-starter-websocket
        • 组成: Java Websocket应用由一系列的Endpoint组成
          • Endpoint是一个java对象,代表WebSocket链接的一端,对于服务端,我们可以视为处理具体Websocket消息的接口
        • Endpoint:
          • 定义:
            • 编程式: 继承类javax.websocket.Endpoint并实现其方法
            • 注解式: 定义一个pojo,并添加@ServerEndpoint相关注解。
          • 生命周期: Endpoint实例在WebSocket握手时创建,并在客户端与服务端链接过程中有效,最后在链接关闭时结束
            • onOpen()/@OnOpen: 当开启一个新的会话时调用,该方法是客户端与服务端握手成功后调用的方法
            • onClose()/@OnClose: 当会话关闭时调用
            • onError()/@OnError: 当连接过程异常时调用
      • 消息处理: 【一般要实现SessionPool管理用户会话】
        • 接受消息:
          • 编程式: 通过添加 MessageHandler 消息处理器来接收消息
          • 注解式: 在定义Endpoint时,通过@OnMessage注解指定接收消息的方法
        • 推送消息: 由RemoteEndpoint完成,其实例由Session维护
          • 通过session.getBasicRemote()获取同步消息发送的实例,然后调用其 sendXxx()方法发送消息
          • 通过session.getAsyncRemote()获取异步消息发送实例,然后调用其 sendXxx()方法发送消息

SockJS + STOMP实现

  • 原生Websocket存在的问题:
    • WebSocket本身定义了两种消息类型(文本和二进制),但并没有规定消息的具体内容和格式
    • 因为没有高层级的线路协议,因此就需要我们定义应用之间所发送消息的语义,还需要确保连接的两端都能遵循这些语义
    • 直接使用WebSocket就很类似于使用TCP套接字来编写Web应用
  • STOMP(Simple Text Oriented Messaging Protocol)介绍:
    • 初衷: 初衷是为脚本语言(如Ruby、Python)和web框架创建一种基于文本的简单异步消息协议
    • 构成: 作为基于Frame的协议, 每个Frame由一个命令(Command)、一组头信息(Headers)和可选的正文(Body)组成
      SEND   //作为COMMAND
      Authorization:Bearer xxx        //作为Headers
      content-type:application/json   //作为Headers
      
      Hello World!  //消息内容,可以多行,直到^@为止  //作为Body
      ^@ //此Frame结束
    • 作用: 允许STOMP客户端与任意STOMP消息代理(Broker)使用一种标准化的消息格式和语义进行交互
  • STOMP On Spring WebSocket: Spring主推的完整的websocket解决方案
    • 使用场景对比
    • 通讯过程:
      • 简单使用
      • 独立代理
    • 作用: 实现更加标准和简单的WebSocket消息传递
      • 跨平台支持:
        • 支持多种语言和平台,易于开发客户端
        • 一般通过sock.js对旧版浏览器实现兼容
      • 简化开发: 开发者可以更加专注于业务逻辑
        • 不需要开发者自己定义消息格式
        • 不需要开发者自己管理消息的格式和通信逻辑,包括连接的维护、心跳、重连等
        • 不需要开发者自己实现对事务性消息传递的支持
        • 支持发布/订阅模式,可通过订阅特定的目的地来接收消息,简化了消息的路由和分发
        • 与Spring MVC等组件无缝集成,提供了丰富的编程模型和路由选项
      • 灵活性: 允许开发者自定义消息头和消息体
      • 安全性: 可以使用Spring Security基于目的地和消息类型在连接阶段进行认证和授权,确保只有合法的用户才能订阅和发送消息
  • 基本使用
    • 前端
    • 后端
      • 基础配置
        @Configuration
        @EnableWebSocketMessageBroker
        public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
            @Override
            public void registerStompEndpoints(StompEndpointRegistry registry){
                //客户端连接端点
                registry.addEndpoint("/websocket")
                        .setAllowedOrigins("*")
                        .withSockJS();
            }
        
            @Override
            public void configureMessageBroker(MessageBrokerRegistry registry) {
                // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送
                registry.enableSimpleBroker("/topic", "/queue");
                // 服务端通知客户端的前缀,可以不设置,默认为user
                registry.setUserDestinationPrefix("/queue");
                // 客户端发送消息的请求前缀,默认为空
                registry.setApplicationDestinationPrefixes("/api/ws");
            }
        
            @Override
            public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
                // 配置消息转换器
                DefaultContentTypeResolver resolver = new DefaultContentTypeResolver();
                resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON);
        
                MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
                converter.setObjectMapper(new ObjectMapper());
                converter.setContentTypeResolver(resolver);
        
                messageConverters.add(converter);
                return false;
            }
        }
      • 消息发送
        @RestController
        public class WebsocketController {
            @Autowired
            private SimpMessagingTemplate template;
        
            @MessageMapping("/send")
            @SendTo("/topic")
            public String say(String msg) {
                return msg;
            }
        
            @GetMapping("/send")
            public String msgReply(@RequestParam String msg) {
                template.convertAndSend("/topic", msg);
                return msg;
            }
        }
        • template.convertAndSend: 可以搭配@GetMapping()等HTTP映射
        • template.convertAndSendToUser: 实际上只是添加前缀和userId
        • @MessageMapping(): 可以省略@SendTo

RabbitMQ STOMP

  • 原因:
    • 需要消息代理:
      • spring是基于内存的,只能实现对stomp指令的简单模拟
      • SimpleBroker不支持集群
      • 消息代理可以实现扩容、高可用、持久化
    • 为什么选用RabbitMQ
      • 性能好
      • 支持特性多,AMQP、STOMP、JMS、MQTT
      • 支持各种集群部署
      • 社区热度高,生态好;
  • 使用
    • 安装插件
      rabbitma-plugins enable rabbitmq_stomp
      rabbitmq-plugins enable rabbitmq_web_stomp
    • 消息发送
      • 注意路径规范
      • template.convertAndSendToUser无法使用

WebSocket认证授权

  • 基本原理:
  • 实现方法:
    • 实现void configureClientInboundChannel(ChannelRegistration registration),添加拦截器
      StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
      ...
      accessor.setUser(()-> loginUser.getUser().getId().toString());
      ...

可靠消息推送

  • 实现方法:
    • 修改客户端的subscribe函数
      stompCLient.subscribe("/queue/user" + userid, function(response){
          ...
          response.ack();
      }, {ack: 'client'});
      • 注意,最好使用持久化的队列

消息推送负载均衡实现

  • 非Stomp方案
  • Stomp方案
  • 微服务方案(Spring Cloud GateWay)

生态工具

Lombok

  • 作用: 通过使用注解来减少Java代码中的样板代码
  • 原理: 编译时通过注解处理器来实现代码的自动生成
  • 依赖: org.projectlombok:lombok
  • 使用:
    @Data  // set/get/tostring ....
    @AllArgsConstructor  // 全参
    @NoArgsConstructor // 无参构造
    @ToString // tostring
    @Accessors(chain = true)  // 链式调用
    @Builder  // 构建者模式创建对象
    @Slf4j  // 日志注解支持
    @RequiredArgsConstructor  // 为final, @NonNull和@JsonProperty 且未被初始化的字段生成构造函数
    public class User {
        private Long id;
        private String userName;
        private Integer sex;
        private LocalDate birthday;
        private Date created;
        private Date modified;
    }

RestTemplate

  • form表单请求
    // 设置请求头,指定请求数据方式
    HttpHeaders headers = new HttpHeaders();
    headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
    // 组装模拟form表单数据
    MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
    map.add("greeting", "你好");
    HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(map, headers);
    ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.POST,
            httpEntity, String.class);
    System.out.println(responseEntity.getBody());
  • json格式请求
    Map<String, Object> params = new HashMap<>();
    params.put("name", repoName);
    params.put("private", false);
    HttpEntity<String> entity = new HttpEntity<>(formatJson(params), headers);
    ResponseEntity<String> response = restTemplate.exchange("https://api.github.com/user/repos", HttpMethod.POST, entity, String.class);

ID生成方式

UUID(通用唯一标识符)

  • 生成方式:
    • 基于时间的UUID(version 1):
      • 使用当前时间戳、主机的 MAC 地址和一些随机数据生成
      • 这种方式生成的 UUID 具有时间顺序性,可以用于需要时间顺序的应用场景
    • 基于MD5哈希的UUID(version 3):
      • 使用指定的命名空间和名称生成基于MD5哈希的 UUID
      • 这种方式生成的 UUID 具有确定性,同样的输入会生成相同的输出
    • 基于SHA-1哈希的UUID(version 5):
      • 与基于MD5哈希的方式类似,但使用SHA-1算法进行哈希
      • 这种方式生成的UUID也具有确定性
    • 纯随机生成的UUID(version 4):
      • 使用随机数据生成,不依赖于时间戳或其他标识符
      • 这种方式生成的UUID无法保证时间顺序性,但具有极低的碰撞概率
  • 使用:
    • java
      UUID id = UUID.randomUUID()
    • sql[推荐使用binary(16)存储]
      -- 生成
      UUID()
      -- 转换
      UUID_TO_BIN(UUID())
      BIN_TO_UUID(UUID())

snowflake(雪花算法)

  • 作用: 在分布式的条件下生成全局唯一的id
  • 原理: 64位ID (42(时间戳)+5(机房ID)+5(机器ID)+12(序列号-同毫秒内重复累加))
  • 评价:
    • 优点: 长度较短、满足分布式需求
    • 缺点: 配置较复杂
  • 实现:
    import java.lang.management.ManagementFactory;
    import java.net.InetAddress;
    import java.net.NetworkInterface;
    
    public class IdWorker {
        // 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
        private final static long twepoch = 1288834974657L;
        // 机器标识位数
        private final static long workerIdBits = 5L;
        // 数据中心标识位数
        private final static long datacenterIdBits = 5L;
        // 机器ID最大值
        private final static long maxWorkerId = ~(-1L << workerIdBits);
        // 数据中心ID最大值
        private final static long maxDatacenterId = ~(-1L << datacenterIdBits);
        // 毫秒内自增位
        private final static long sequenceBits = 12L;
        // 机器ID偏左移12位
        private final static long workerIdShift = sequenceBits;
        // 数据中心ID左移17位
        private final static long datacenterIdShift = sequenceBits + workerIdBits;
        // 时间毫秒左移22位
        private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
    
        private final static long sequenceMask = ~(-1L << sequenceBits);
        /* 上次生产id时间戳 */
        private static long lastTimestamp = -1L;
        //同毫秒并发控制
        private long sequence = 0L;
        //机器ID
        private final long workerId;
        //机房ID
        private final long datacenterId;
    
        public IdWorker(){
            this.datacenterId = getDatacenterId(maxDatacenterId);
            this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
        }
        /**
         * @param workerId
         *            工作机器ID
         * @param datacenterId
         *            序列号
         */
        public IdWorker(long workerId, long datacenterId) {
            if (workerId > maxWorkerId || workerId < 0) {
                throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
            }
            if (datacenterId > maxDatacenterId || datacenterId < 0) {
                throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
            }
            this.workerId = workerId;
            this.datacenterId = datacenterId;
        }
        /**
         * @return 下一个ID
         */
        public synchronized long nextId() {
            long timestamp = timeGen();
            if (timestamp < lastTimestamp) {
                throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
            }
    
            if (lastTimestamp == timestamp) {
                // 当前毫秒内,则+1
                sequence = (sequence + 1) & sequenceMask;
                if (sequence == 0) {
                    // 当前毫秒内计数满了,则等待下一秒
                    timestamp = tilNextMillis(lastTimestamp);
                }
            } else {
                sequence = 0L;
            }
            lastTimestamp = timestamp;
            // ID偏移组合生成并返回最终的ID,并返回ID
            return ((timestamp - twepoch) << timestampLeftShift)
                    | (datacenterId << datacenterIdShift)
                    | (workerId << workerIdShift) | sequence;
        }
    
        private long tilNextMillis(final long lastTimestamp) {
            long timestamp = this.timeGen();
            while (timestamp <= lastTimestamp) {
                timestamp = this.timeGen();
            }
            return timestamp;
        }
    
        private long timeGen() {
            return System.currentTimeMillis();
        }
    
        /**
         * <p>
         * 获取 maxWorkerId
         * </p>
         */
        protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
            StringBuffer mpid = new StringBuffer();
            mpid.append(datacenterId);
            String name = ManagementFactory.getRuntimeMXBean().getName();
            if (!name.isEmpty()) {
                /*
                 * GET jvmPid
                 */
                mpid.append(name.split("@")[0]);
            }
            /*
             * MAC + PID 的 hashcode 获取16个低位
             */
            return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
        }
    
        /**
         * <p>
         * 数据标识id部分
         * </p>
         */
        protected static long getDatacenterId(long maxDatacenterId) {
            long id = 0L;
            try {
                InetAddress ip = InetAddress.getLocalHost();
                NetworkInterface network = NetworkInterface.getByInetAddress(ip);
                if (network == null) {
                    id = 1L;
                } else {
                    byte[] mac = network.getHardwareAddress();
                    id = ((0x000000FF & (long) mac[mac.length - 1])
                            | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
                    id = id % (maxDatacenterId + 1);
                }
            } catch (Exception e) {
                System.out.println(" getDatacenterId: " + e.getMessage());
            }
            return id;
        }
    }
  • 使用:
    • IdWorker实例放在bean中管理
    • 分布式部署时分配好机房id和机器id

MyBatisPlus

基本介绍

  • 定位: 辅助Mybatis,简化dao层开发
  • 官网: https://mybatis.plus/
  • 特性:
    • 实现基本CURD: 内置通用Mapper、通用Service,仅仅通过少量配置即可实现单表大部分CRUD操作,更有强大的条件构造器,满足各类使用需求
    • 支持主键自动生成: 支持4种主键策略,可自由配置,且支持回填
    • 支持ActiveRecord模式: 支持ActiveRecord形式调用,实体类只需继承Model类即可进行强大的CRUD操作
    • 支持自定义全局通用操作: 支持全局通用方法注入(Write once, use anywhere)
    • 内置代码生成器: 采用代码或者Maven插件可快速生成 Mapper、Model、Service、Controller层代码,支持模板引擎,更有超多自定义配置等您来使用
    • 内置全局拦截插件:
      • 防全表更新与删除插件: 提供全表deleteupdate操作智能分析阻断,也可自定义拦截规则,预防误操作
      • 内置性能分析插件: 可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
      • 内置分页插件: 基于MyBatis物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通List查询
      • 乐观锁插件: 提供版本号字段设置和更新
      • 动态表名插件: 允许在运行时动态地改变 SQL 语句中的表名,对分表非常有用
  • 样例: baomidou/mybatis-plus-samples

po相关

  • 模型注解: @TableName()
  • 字段注解:
    • @TableId(value, type): 可以配置主键生成策略
    • @TableField(value, exist, fill): 自动填充
    • @TableLogic: 配置逻辑删除
      • 逻辑删除的本质: 逻辑删除的效果应等同于物理删除,其目的是为了保留数据,实现数据价值最大化
      • 业务需求考量: 如果业务中仍需频繁查询这些“已删除”的数据,应考虑是否真正需要逻辑删除
    • @Version: 配置乐观锁
    • @EnumValue: 自动映射枚举

dao相关

  • BaseMapper<T>
    • 删除: deleteById, deleteBatchIds, deleteByMap, delete(wrapper)
    • 更新: updateById, update(po, wrapper)
    • 分页: selectPage(page, wrapper)
  • 自定义方法中使用分页
    IPage<User> selectAllByPage(IPage<User> page);
    <select id="selectAllByPage" resultType="User">
        select <include refid="commonFields" />
        from user
    </select>
    IPage<User> page = new Page<>(4, 10);
    userMapper.selectAllByPage(page);
    System.out.println(page.getRecords().size());
  • Wrapper:【与很多功能不兼容,且会带来分层的混乱,并不推荐使用】
    • QueryWrapperLambdaQueryWrapper
      QueryWrapper<User> wrapper = new QueryWrapper<>();
      wrapper.like("user_name", "伤")
      .eq("password", "123456")
      .in("age", 19, 25, 29)
      .in("age", Arrays.asList(19,25,29))
      .orderByDesc("age");
      List<User> users = userMapper.selectList(wrapper);
      System.out.println(users);
      LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
      wrapper.eq(User::getUsername, "henry");
      List<User> users = userMapper.selectList(wrapper);
      • 常用API
        • eq, ne, gt, ge, lt, le
        • between, notBetween, in, notIn【要注意,反向查询不走索引】
        • like(column, condition), likeLeft(column, condition), likeRight(column, condition)
        • orderByAsc, orderByDesc
        • select(String...)
      • 多条件查询时,默认使用and关键字拼接SQL; 调用or()方法时,底层会使用or关键字拼接方法左右的查询条件
    • LambdaUpdateWrapper
      LambdaUpdateWrapper<User> wrapper = Wrappers.lambdaUpdate();
      wrapper.eq(user::getId, 41).set(User::getPassword, "66666")
      .set(User::getAge, 24);
      int update = userMapper.update(null, wrapper);
      System.out.println(update);

service相关

  • 源码:
    • ServiceImpl:
      public abstract class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {
          protected final Log log = LogFactory.getLog(this.getClass());
          @Autowired
          protected M baseMapper;
          private Class<T> entityClass;
          private Class<M> mapperClass;
          private volatile SqlSessionFactory sqlSessionFactory;
      
          public ServiceImpl() {
          }
      }
  • 使用:
    • CustomService接口继承IService
      public interface UserService extends IService<User> {}
    • CustomServiceImpl继承ServiceImpl并实现CustomService接口
      @Service
      public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {}

接口文档生成与分享

Swagger

  • 作用: 生成、描述、调用和可视化RESTful风格的Web服务
  • 功能:
    • 使得前后端分离开发更加方便,有利于团队协作
    • 接口文档在线自动生成,降低后端开发人员编写接口文档的负担
    • 接口功能测试
  • 使用:

Knife4j

  • 官网: https://doc.xiaominfo.com/
  • 定位: 集Swagger2和OpenAPI3为一体的API文档增强解决方案
  • 功能:
    • 文档说明: 详细列出接口文档的说明,包括接口地址、类型、请求示例、请求参数、响应示例、响应参数、响应码等信息
    • 离线文档: 生成的在线markdown/html离线文档
    • 接口排序
    • 在线调试: 在线接口联调,自动解析当前接口参数,同时包含表单验证,调用参数可返回接口响应内容、headers、Curl请求命令实例、响应时间、响应状态码等信息

YApi

  • 官网: https://github.com/YMFE/yapi
  • 作用:
    • 文档管理【降低对后端服务的依赖,方便团队合作】
    • 接口调试【需要安装跨域插件】
    • Mock数据
    • 自动化测试

微服务架构

  • 分布式架构

    • 定义: 根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,称为一个服务
    • 评价:
      • 缺点:
        • 架构复杂
        • 部署难度高
      • 优点:
        • 耦合度低,维护成本低
        • 有利于服务升级拓展【高负载模块可以单独搞集群】
    • 问题:
      • 服务拆分粒度如何
      • 服务集群地址如何维护
      • 服务之间如何实现远程调用
      • 服务健康状态如何感知
    • 微服务架构:
    • 特征:
      • 单一职责: 微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责
      • 自治: 团队独立、技术独立、数据独立,独立部署和交付
      • 面向服务: 服务提供统一标准的接口,与语言和技术无关
      • 隔离性强: 服务调用做好隔离、容错、降级,避免出现级联问题
    • 评价:
      • 优点: 拆分粒度更小、服务更独立、耦合度更低
      • 缺点: 架构非常复杂,运维、监控、部署难度提高
    • 组成:
      • 服务网关
      • 注册中心
      • 配置中心
      • 服务集群
    • 技术
编写
预览