三层架构: 这种架构的设计有助于实现代码的模块化、可维护性和可扩展性
- 表现层(
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
】
- Persistent Object(po): 早期会用,不仅与表一一对应,还会直接提供操作表的方式【e.g.
- 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
=>username
或userName
】<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; } }
- 使用
SqlProvider
和org.apache.ibatis.jdbc.SQL
【SQL
类语法复杂,不推荐使用】
@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]
- mac为
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>
- XML开发:
- 生命周期:【单例设计模式】
- 创建:
- 步骤:
- 加载
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新特性:
- 支持
Servlet
、Filter
、Listener
注解配置 - 支持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数据或者用户登录后,才创建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}
- 从jsp域对象(pageContext,request,session,application)中获取数据:
- 相关操作
- 算术、关系、逻辑、三元运算
empty
和not empty
【空字符串、空集合、空对象】
- 确保启用EL:
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>
顺序执行
- 如果是xml配置,按照xml中
- 过滤顺序: 与匹配优先级无关
监听器(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); } }
- 典型案例: 加载初始化参数(类似Spring框架)
### JSON处理
- 工具库:
jackson
、fastjson
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: 两个项目中的
HondaCar
与TeslaCar
分别实现了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"); } }
- 场景1: 两个项目中的
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., 调试器调试运行系统是通过运行系统通知调试器来实现的】
- 定义: Inversion of Control(控制反转) 由主动
- 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);
- 使用bean名称获取:
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
: 分为singleton
、prototype
、request
、session
、application
、websocket
<bean id="bookDao" class="com.morningstar.dao.impl.BookDaoImpl" scope="prototype"></bean>
- 适合交给容器进行管理的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
方法就必须使用构造器注入
- 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"/>
- 自动: 实现
InitializingBean
与DisposableBean
接口后就可以自动适配对应钩子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))
实现构造器注入】- 默认先
byType
,根据类型找到需要的对象,放入一个Map中 - 判断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
结尾的接口中的任意方法 - 所有带有一个参数的方法
- 所有的
- 一个具体方法:
- 在SpringAOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法
- 切面(Aspect): 描述通知与切入点的对应关系
- 通知(Advice): 在切入点处执行的操作,也就是共性功能
- 在SpringAOP中,功能最终以方法的形式呈现
- 通知(Advice): 在切入点处执行的操作,也就是共性功能
- 切入点(Pointcut): 匹配连接点的式子
- 目标对象与代理:
- 区分:
- 目标对象(Target): 原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
- 代理(Proxy): 目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现
- 现象:
- 如果
UserDaoImpl
类中的方法被增强了,容器中不会存在UserDaoImpl.class
,只会存在它的代理对象,只能使用UserDao.class
或者名称来访问
- 如果
- 区分:
- 织入: 将增强添加对目标类具体连接点上的过程
- 编译期织入: 要求使用特殊的Java编译器
- 类装载期织入: 这要求使用特殊的类装载器
- 动态代理织入: 在运行期为目标类添加增强生成子类的方式【Spring默认】
- 连接点(JoinPoint): 程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
- 相关注解:
@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注解式事务通常添加在业务层接口中而不会添加到业务层实现类中,降低耦合
- SpringConfig上开启事务管理:
- 事务属性配置:
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框架
- 优点:
- 使用简单,开发便捷(相比于
Servlet
或BaseServlet
) - 灵活性强
- 使用简单,开发便捷(相比于
- 使用步骤:
- 建项目并导包
<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
- 嵌套pojo的传参:
- 数组:
@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"; }
- Url传参(
- 解决中文乱码:
- 请求行参数:【一般不传送中文数据】
<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); }
- 文档: 核心技术 (spring.io)
HttpMessageConverter
: 用于转换HTTP请求和响应【实现了JSON、XML等数据类型的转换】- 文档: Web on Servlet Stack (spring.io)
- 注意: 需要开启
@EnableWebMvc
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
执行继承自AbstractDispatcherServletInitializer
的registerDispatcherServlet
方法注册DispatcherServlet
- 通过SPI加载
ServletContainerInitializer
的实现类org. springframework.web.SpringServletContainerInitializer
- 该实现类会查找系统中
WebApplicationInitializer
接口的实现类,并依次调用它们的onStartup
方法@HandlesTypes(WebApplicationInitializer.class) public class SpringServletContainerInitializer implements ServletContainerInitializer{}
- 通过SPI加载
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
后不再走视图解析器)
- 非空: 调用视图解析器
- Servlet配置类
SSM整合
需求:
- 整合Spring、SpringMVC、Mybatis、Junit
- 使用
Result
统一表现层响应结果: 将json响应体分为code
、msg
和data
- 整合静态资源
代码:
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>--> <!-- <!– put your configurations here –>--> <!-- <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
- yaml配置
- 带参启动:
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
【最低】
- 1级:
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.Servlet
中getEncoding()
获取的Encoding.DEFAULT_CHARSET
是StandardCharsets.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
- 案例: 以下代码会加载
Book
Bean@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
替换底层的组件 - 用户去看这个组件是获取的配置文件什么值就去修改
- 程序启动找到自动化配置包下
- 开发人员使用步骤总结:
健康监控
健康监控数据服务
- 依赖:
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>
- 直接根据Maven坐标
- 使用:
- 配置
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同时满足了两个队列的匹配模式
- 消息具有以下headers:
- 特点: 提供了比direct更复杂的路由能力,但可能在性能上不如direct 交换机,因为它需要进行更多的属性比较
- Topic Exchange(主题): 绑定RoutingKey的时候使用通配符
- RoutingKey一般都是有一个或多个单词组成,多个单词之间以”.”分割(e.g.
item.insert
)#
: 匹配一个或多个词*
: 匹配恰好一个词
- 接收的消息RoutingKey必须是多个单词,以
.
分割
- RoutingKey一般都是有一个或多个单词组成,多个单词之间以”.”分割(e.g.
- 远程过程调用协议(Remote Procedure Call, RPC): 允许程序调用另一台计算机上的程序或服务上的方法,就像调用本地程序一样简单的技术
- RPC协议旨在简化跨网络的服务调用过程
- 使得开发者在使用远程服务时,不必关心底层的网络通信细节,而是能够像调用本机上的方法一样方便
- 远程方法调用(Remote Method Invocation,RMI): 计算机之间利用远程对象互相调用实现双方通讯的一种通讯机制
- 地位: Enterprise JavaBeans的支柱,是建立分布式Java应用程序的方便途径
- 特点: RPC未能做到面向对象调用的开发模式,而RMI允许程序员使用远程对象来实现通信,并且支持多线程的服务
- 协议: 默认采用的是TCP/IP
- 基本消息队列(Basic Queue)
- 具体实现:【简单队列模式】
- 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("等待接收消息。。。。");
- publisher: 建立connection -> 创建channel -> 声明队列 -> 向队列发送消息 -> 关闭connection和channel
- 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底层的默认实现【提供类似
RedisTemplate
的RabbitTemplate
】
环境配置
- 配置依赖与连接
<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
- 依赖:
com.github.ben-manes.caffeine:caffeine
- 原理: Caffeine Cache-高性能Java本地缓存组件
认证与授权
认证与授权
基本概念
- 表设计: 经典权限五张表
- 权限 -> 职责【隐含资源信息】
- 用户 -> 职员
- 角色 -> 职位
- 权限模型
- RBAC(Role-Based Access Control):
- 缺点: 根据角色判断权限容易产生许多修改,不满足开闭原则
- RBAC(Resource-Based Access Control):
- 优点: 资源操作标签不容易变动,这样不需要改源码,只需修改数据库
- RBAC(Role-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)
- 头部(Header)(非敏感): 用于描述关于该JWT的最基本的信息,例如数据类型以及签名所用的算法等【e.g.
- 评价: 推荐阅读一文带你搞懂JWT常见概念 &优缺点
- 优点:
- 使用json作为数据传输,有广泛的通用型,并且体积小,便于传输
- 不需要在服务器端保存相关信息,节省内存资源的开销
- jwt载荷部分可以存储业务相关的信息(非敏感的),例如用户信息、角色等
- 如果使用非对称加密,可以用公钥来验证签名,适合大型/分布式系统
- 缺点:
- 注销登录等场景下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
- 在请求处理完成后,将
SecurityContext
从SecurityContextHolder
中取出,并存储到SecurityContextRepository
中,同时清除securityContextHolder
所持有的SecurityContext
- 在存储
SecurityContext
的过程中,会确保JSESSIONID Cookie
被正确地设置 - 在存储
SecurityContext
的过程中,通过HttpSessionSecurityContextRepository
类将SecurityContext
数据存储到HTTP Session中,并在此过程中设置 JSESSIONID Cookie
- 在存储
- 在请求进入应用程序时从
- 原生的认证过滤器(
UsernamePasswordAuthenticationFilter
)用于处理来自表单提交的认证,该表单必须提供username
和password
,其内部还有登录成功或失败后进行处理的AuthenticationSuccessHandler
和AuthenticationFailureHandler
,处理流程如下- 认证过滤器接收form表单提交的账户、密码信息,并封装成
UsernamePasswordAuthenticationToken
认证凭据对象 - 认证过滤器调用认证管理器
AuthenticationManager
进行认证处理 - 认证管理器通过调用
UserDetailsService
获取用户详情UserDetails
- 认证管理器通过密码匹配器
PasswordEncoder
进行匹配,如果密码一致,则将用户相关的权限信息一并封装到Authentication
认证对象中【存入principal
属性】 - 认证过滤器将
Authentication
认证对象放到安全上下文SecurityContextHolder
(基于ThreadLocal
),方便请求从上下文获取认证信息
- 认证过滤器接收form表单提交的账户、密码信息,并封装成
ExceptionTranslationFilter
能够捕获来自FilterChain
所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException
和AccessDeniedException
,其它的异常它会继续抛出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失效
- 根据请求中的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
: 主要作用在于提供基础的防护,减少恶意连接、意外连接
- 特性:
- 优点: 全双工、长连接
- 缺点: 连接维持成本、重连等问题
- 应用: 实时游戏、网页聊天、体育实况、股票行情、视频弹幕、实时更新
- 相关header:
- 基本使用
- 客户端
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
): 连接出错时触发
- open(
- 相关方法:
send()
: 通过websocket对象调用该方法发送数据给服务端
- 异常处理: 连接关闭或出错后设置定时重连【避免过多重连】
- 心跳检测: 在
onopen
与onmessage
的方法末尾调用heartCheck.start()
- WS对象相关事件
- 服务端
@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()
方法发送消息
- 通过
- 接受消息:
- API:
- 客户端
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无法保证时间顺序性,但具有极低的碰撞概率
- 基于时间的UUID(version 1):
- 使用:
- java
UUID id = UUID.randomUUID()
- sql[推荐使用
binary(16)
存储]-- 生成 UUID() -- 转换 UUID_TO_BIN(UUID()) BIN_TO_UUID(UUID())
- java
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层代码,支持模板引擎,更有超多自定义配置等您来使用
- 内置全局拦截插件:
- 防全表更新与删除插件: 提供全表
delete
、update
操作智能分析阻断,也可自定义拦截规则,预防误操作 - 内置性能分析插件: 可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
- 内置分页插件: 基于MyBatis物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通List查询
- 乐观锁插件: 提供版本号字段设置和更新
- 动态表名插件: 允许在运行时动态地改变 SQL 语句中的表名,对分表非常有用
- 防全表更新与删除插件: 提供全表
- 样例: baomidou/mybatis-plus-samples
po相关
- 模型注解:
@TableName()
- 字段注解:
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
:【与很多功能不兼容,且会带来分层的混乱,并不推荐使用】QueryWrapper
与LambdaQueryWrapper
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关键字拼接方法左右的查询条件
- 常用API
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服务
- 功能:
- 使得前后端分离开发更加方便,有利于团队协作
- 接口文档在线自动生成,降低后端开发人员编写接口文档的负担
- 接口功能测试
- 使用:
- 注意: SpringBoot3版本依赖于jakarta,但是Swagger依赖javax
- 参考:
- 步骤:
- 引入依赖
- 定义swagger配置类
- 配置swagger扫描管理的web资源路径
- 配置项目文档标题、描述、版本等信息、官网地址等信息
- 通过swagger注解给指定资源添加描述信息
- 项目启动,访问并测试在线资源
- UI:
http://server:port/context-path/swagger-ui.html
- JSON:
http://server:port/context-path/v3/api-docs
- UI:
Knife4j
- 官网: https://doc.xiaominfo.com/
- 定位: 集Swagger2和OpenAPI3为一体的API文档增强解决方案
- 功能:
- 文档说明: 详细列出接口文档的说明,包括接口地址、类型、请求示例、请求参数、响应示例、响应参数、响应码等信息
- 离线文档: 生成的在线markdown/html离线文档
- 接口排序
- 在线调试: 在线接口联调,自动解析当前接口参数,同时包含表单验证,调用参数可返回接口响应内容、headers、Curl请求命令实例、响应时间、响应状态码等信息
YApi
- 官网: https://github.com/YMFE/yapi
- 作用:
- 文档管理【降低对后端服务的依赖,方便团队合作】
- 接口调试【需要安装跨域插件】
- Mock数据
- 自动化测试
微服务架构
-
分布式架构
- 定义: 根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,称为一个服务
- 评价:
- 缺点:
- 架构复杂
- 部署难度高
- 优点:
- 耦合度低,维护成本低
- 有利于服务升级拓展【高负载模块可以单独搞集群】
- 缺点:
- 问题:
- 服务拆分粒度如何
- 服务集群地址如何维护
- 服务之间如何实现远程调用
- 服务健康状态如何感知
- 微服务架构:
- 特征:
- 单一职责: 微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责
- 自治: 团队独立、技术独立、数据独立,独立部署和交付
- 面向服务: 服务提供统一标准的接口,与语言和技术无关
- 隔离性强: 服务调用做好隔离、容错、降级,避免出现级联问题
- 评价:
- 优点: 拆分粒度更小、服务更独立、耦合度更低
- 缺点: 架构非常复杂,运维、监控、部署难度提高
- 组成:
- 服务网关
- 注册中心
- 配置中心
- 服务集群
- 技术