cms项目(3)-日志模块+MyBatis插件开发+前后台首页+线程池应用
笔记中涉及资源:
提取码:Coke
一、数据库中添加日志表
①:添加表
cms-portal/src/main/resources/migration/V1__Base_version.sql
- 添加以下sql语句
-- ----------------------------
-- Table structure for cms_log 日志表
-- ----------------------------
CREATE TABLE cms_log
(
create_time timestamp not null default CURRENT_TIMESTAMP,
update_time timestamp not null default '0000-00-00 00:00:00',
id int(11) NOT NULL AUTO_INCREMENT primary key,
user_id int(11) not null comment '用户id',
username varchar(25) not null comment '用户名称',
login_ip varchar(30) default '' comment 'ip地址',
url varchar(100) default '' comment 'URL地址',
content varchar(100) null comment '日志内容'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
删除原有的表,运行程序重新生成表
运行程序生成新表
二、持久层和应用层代码开发
①:持久层
1. 创建实体类和Dto
com.it.dao.entity.CmsLogEntity
@Getter
@Setter
public class CmsLogEntity extends BaseEntity<Integer>{
private Integer userId;
private String username;
private String loginIp;
private String url;
private String content;
}
com.it.service.dto.CmsLogDto
@Getter
@Setter
public class CmsLogDto extends BaseDto<Integer> {
private Integer userId;
private String username;
private String loginIp;
private String url;
private String content;
}
2. 定义converter转换规则
com.it.service.converter.CmsLogConverter
@Mapper
public interface CmsLogConverter {
CmsLogConverter CONVERTER = Mappers.getMapper(CmsLogConverter.class);
CmsLogEntity dtoToEntity(CmsLogDto dto);
// CmsLogDto entityToDto(CmsLogEntity entity);
}
com.it.dao.enums.EnumConverter 添加以下方法
/**
* 将dto中的枚举转换成entity中的Integer
* @param enumeration 枚举
* @param <E> 泛型
* @return Integer
*/
public static <E extends BaseEnum> Integer toInteger(E enumeration){
return (enumeration != null)? enumeration.getOrdinal() : null;
}
3. 创建Mapper接口和xml文件
com.it.core.BaseMapper 中添加save方法
/**
* 统一添加
* @param entity log实体
*/
void save(ENTITY entity);
com.it.dao.mapper.CmsLogMapper
public interface CmsLogMapper extends BaseMapper<CmsLogEntity, Integer>{
}
cms-dao/src/main/java/com/it/dao/mapper/mappers/CmsLogMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.it.dao.mapper.CmsLogMapper">
<insert id="save">
<selectKey order="AFTER" keyProperty="id" resultType="java.lang.Integer">
select last_insert_id()
</selectKey>
insert into cms_log(create_time, user_id, username, login_ip, url, content)
values(#{createTime}, #{userId}, #{username}, #{loginIp}, #{url}, #{content})
</insert>
</mapper>
selectKey 会将 select last_insert_id()的结果放入到传入的model的主键里面,keyProperty 对应的model中的主键的属性名,这里是 log 中的id,因为它跟数据库的主键对应order AFTER 表示 SELECT LASTINSERTID() 在insert执行之后执行,多用与自增主键,BEFORE表示SELECT LASTINSERTID() 在insert执行之前执行,这样的话就拿不到主键了,这种适合那种主键不是自增的类型resultType 主键类型
②:应用层
1. 创建service接口和实现类
com.it.core.BaseService 中添加save方法
/**
* 统一添加
* @param dto 实体类
*/
void save(DTO dto);
com.it.service.api.CmsLogService
public interface CmsLogService extends BaseService<CmsLogDto, Integer> {
}
com.it.service.impl.CmsLogServiceImpl
@Service
public class CmsLogServiceImpl implements CmsLogService {
@Autowired
private CmsLogMapper cmsLogMapper;
@Override
public void save(CmsLogDto dto) {
CmsLogEntity cmsLogEntity = CmsLogConverter.CONVERTER.dtoToEntity(dto);
cmsLogMapper.save(cmsLogEntity);
}
@Override
public CmsLogDto getById(Integer id) {
return null;
}
}
2. 封装获取IP地址的方法
com.it.context.utils.UtilsHttp 中添加以下方法
private static final String HEADER_REAL_IP = "X-Real-IP";
private static final String UNKNOWN = "unknown";
private static final String SLASH = "../";
private static final String BACKSLASH = "..\";
private static final String HEADER_FORWARDED_FOR = "X-Forwarded-For";
private static final String COMMA = ",";
private static final String IP_EMPTY = "0:0:0:0:0:0:0:1";
private static final String IP_LOOP = "127.0.0.1";
/**
* 获取访问者IP
* 在一般情况下使用Request.getRemoteAddress()即可,但是经过nginx等反向代理软件后,这个方法会失效。
* 本方法先从Header中获取X-Real-IP,如果不存在再从X-Forwarded-For获得第一个IP(用,分割),
* 如果还不存在则调用Request .getRemoteAddress()。
*
* @return string
*/
public static String getRemoteAddress() {
HttpServletRequest request = getRequest();
String ip = request.getHeader(HEADER_REAL_IP);
if (!StringUtils.isBlank(ip) && !UNKNOWN.equalsIgnoreCase(ip)) {
if (ip.contains(SLASH) || ip.contains(BACKSLASH)) {
return "";
}
return ip;
}
ip = request.getHeader(HEADER_FORWARDED_FOR);
if (!StringUtils.isBlank(ip) && !UNKNOWN.equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个IP值,第一个为真实IP。
int index = ip.indexOf(COMMA);
if (index != -1) {
ip = ip.substring(0, index);
}
if (ip.contains(SLASH) || ip.contains(BACKSLASH)) {
return "";
}
return ip;
} else {
ip = request.getRemoteAddr();
if (ip.contains(SLASH) || ip.contains(BACKSLASH)) {
return "";
}
if (ip.equals(IP_EMPTY)) {
ip = IP_LOOP;
}
return ip;
}
}
三、添加日志时更新副表内容
①:持久层
1. 创建Mapper接口和xml文件
com.it.dao.entity.CmsUserEntity
com.it.service.dto.CmsUserDto
都添加以下两个字段
private String sessionId;
private String lastLoginIp;
com.it.service.dto.CmsLogDto
public static CmsLogDto of(Integer userId, String username, String loginIp, String url, String content) {
CmsLogDto cmsLogDto = new CmsLogDto();
cmsLogDto.setUserId(userId);
cmsLogDto.setUsername(username);
cmsLogDto.setLoginIp(loginIp);
cmsLogDto.setUrl(url);
cmsLogDto.setContent(content);
cmsLogDto.setCreateTime(LocalDateTime.now());
cmsLogDto.setUpdateTime(LocalDateTime.now());
return cmsLogDto;
}
com.it.core.BaseMapper
/**
* 统一修改
* @param entity 实体类
*/
void update(ENTITY entity);
/src/main/java/com/it/dao/mapper/mappers/CmsUserMapper.xml
<sql id="updateField">
update_time = #{updateTime},
<if test="sessionId != null and sessionId != ''">
session_id = #{sessionId},
</if>
<if test="lastLoginIp != null and lastLoginIp != ''">
last_login_ip = #{lastLoginIp},
</if>
</sql>
<update id="update">
update cms_user
<trim prefix="set" suffixOverrides=",">
<include refid="updateField"/>
</trim>
where id = #{id}
</update>
2. 配置web.xml文件 便于获取ip地址
<!--web容器的启动和关闭监听 该监听器监听HTTP请求事件,web服务器接收的每一次请求都会通知该监听器-->
<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
②:引用层
1. 创建Service接口和实现类
com.it.core.BaseService
/**
* 统一修改
* @param dto 实体类
*/
void update(DTO dto);
com.it.service.api.CmsUserService 继承BaseService
public interface CmsUserService extends BaseService<CmsUserDto, Integer> {
......
}
com.it.service.converter.CmsUserConverter
@Mapper(uses = EnumConverter.class)
public interface CmsUserConverter {
CmsUserConverter CONVERTER = Mappers.getMapper(CmsUserConverter.class);
CmsUserEntity dtoToEntity(CmsUserDto cmsUserDto);
CmsUserDto entityToDto(CmsUserEntity entity);
}
com.it.service.impl.CmsUserServiceImpl 中添加以下方法
@Override
public void save(CmsUserDto dto) {
CmsUserEntity cmsUserEntity = CmsUserConverter.CONVERTER.dtoToEntity(dto);
cmsUserMapper.save(cmsUserEntity);
}
@Override
public void update(CmsUserDto dto) {
CmsUserEntity cmsUserEntity = CmsUserConverter.CONVERTER.dtoToEntity(dto);
cmsUserMapper.update(cmsUserEntity);
}
③:控制层
1. 重写onLoginSuccess方法记录日志
com.it.portal.security.filter.CmsAuthenticationFilter
public class CmsAuthenticationFilter extends FormAuthenticationFilter{
@Autowired
private CommonService commonService;
@Autowired
private CmsUserService cmsUserService;
@Autowired
private CmsLogService cmsLogService;
@Override
protected boolean isLoginRequest(ServletRequest request, ServletResponse response) {
return super.isLoginRequest(request, response) ||
this.pathsMatch("/admin/cms/login.do", request);
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json; charset=UTF-8");
PrintWriter writer = response.getWriter();
// 获取验证码
String captcha = WebUtils.getCleanParam(request, "captcha");
String imageCaptcha = commonService.verifyImageCaptcha(captcha);
if (1 >2 &&Objects.nonNull(imageCaptcha)) {
writer.write(JSON.toJSONString(Result.fail(imageCaptcha,null)));
writer.close();
return false;
}
Subject subject = UtilsShiro.getSubject();
AuthenticationToken token = this.createToken(request, response);
try {
// 使用token登录
subject.login(token);
// 记录日志
onLoginSuccess(token,subject,request,response);
writer.write(JSON.toJSONString(Result.success("登录成功!")));
} catch (IncorrectCredentialsException | UnknownAccountException e) {
writer.write(JSON.toJSONString(Result.fail("用户名或密码错误!",null)));
}catch (DisabledAccountException e){
writer.write(JSON.toJSONString(Result.fail(e.getMessage(),null)));
}catch (Exception e){
// 用户有可能已经登录 其他错误
if (subject.isAuthenticated()){
// 已经登录
writer.write(JSON.toJSONString(Result.success("登录成功!")));
}else {
writer.write(JSON.toJSONString(Result.fail("网络连接失败,请重新登录!",null)));
}
}finally {
writer.close();
}
return false;
}
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String requestURI = httpServletRequest.getRequestURI();
String ip = UtilsHttp.getRemoteAddress();
CmsUserDto cmsUserDto = (CmsUserDto) subject.getPrincipal();
cmsUserDto.setLastLoginIp(ip);
cmsUserDto.setSessionId(UtilsShiro.getSession().getId().toString());
cmsUserDto.setUpdateTime(LocalDateTime.now());
cmsUserService.update(cmsUserDto);
cmsLogService.save(CmsLogDto.of(cmsUserDto.getId(),cmsUserDto.getUsername(),ip,requestURI,"用户后台系统登录!"));
return false;
}
}
2. 测试
四、MyBatis插件机制
①:背景
关于Mybatis插件,大部分人都知道,也都使用过,但很多时候,我们仅仅是停留在表面上,知道Mybatis插件可以在DAO层进行拦截,如打印执行的SQL语句日志,做一些权限控制,分页等功能;但对其内部实现机制,涉及的软件设计模式,编程思想往往没有深入的理解。
②:MyBatis插件原理
Mybatis的插件其实就是个拦截器功能。它利用JDK动态代理和责任链设计模式的综合运用。采用责任链模式,通过动态代理组织多个拦截器,通过这些拦截器你可以做一些你想做的事.
③:适用场景
分页功能
mybatis的分页默认是基于内存分页的(查出所有,再截取),数据量大的情况下效率较低,不过使用mybatis插件可以改变该行为,只需要拦截StatementHandler类的prepare方法,改变要执行的sQL语句为分页语句即可;
公共字段统一赋值
一般业务系统都会有创建者,创建时间,修改者,修改时间四个字段,对于这四个字段的赋值,实际上可以在DAO层统一拦截处理,可以用mybatis插件拦截Executor类的update方法,对相关参数进行统一赋值即可;
性能监控
对于sQL语句执行的性能监控,可以通过拦截Executor类的update,query等方法,用日志记录每个方法执行的时间;
其它
其实mybatis扩展性还是很强的,基于插件机制,基本上可以控制sQL执行的各个阶段,如执行阶段,参数处理阶段,语法构建阶段,结果集处理阶段,具体可以根据项目业务来实现对应业务逻辑。
④:MyBatis插件介绍
1.什么是Mybatis插件
与其称为Mybatis插件,不如叫Mybatis拦截器,更加符合其功能定位,实际上它就是一个拦截器,应用代理模式,在方法级别上进行拦截。
2.支持拦截的方法
执行器Executor(update、query、commit、rollback等方法);
参数处理器ParameterHandler(getParameterObjectsetParameters方法);
结果集处理器ResultSetHandler (handleResultSets、handleOutputParameters等方法);
SQL语法构建器StatementHandler(prepare、parameterize、batch、update、query等方法);
3.使用mybatis插件:
01. 实现Interceptor接囗:
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
详细说说这3个方法:
-
intercept: 在此实现自己的拦截逻辑,可从Invocation参数中拿到执行方法的对象,方法,方法参数,从而实现各种业务逻辑,如下代码所示,从invocation中获取的statementHandler对象即为被代理对象,基于该对象,我们获取到了执行的原始sQL语句,以及prepare方法上的分页参数,并更改SQL语句为新的分页语句,最后调用invocation.proceed()返回结果。
-
plugin:生成代理对象
-
setProperties:允许在plugin元素中配置所需参数,该方法在插件初始化的时候会被调用一次;
02. 插件初始化:
插件的初始化时在MyBatis初始化的时候完成的,读入插件节点和配置的参数,使用反射技术生成插件实例,然后调用插件方法中的setProperties方法设置参数,并将插件实例保存到配置对象中,具体过程看下面代码。
<!--配置mybatis插件-->
<property name="plugins">
<array>
<bean class="com.cms.context.interceptor.BaseInterceptor"/>
</array>
</property>
03. 注解解释:
@Intercepts:标识它是一个拦截器,在实现Interceptor接口的类上声明,使该类注册成为拦截器.
@Signature:注册拦截器签名的地方
public @interface Signature {
Class<?> type(); //要拦截的类
String method(); // 拦截方法
Class<?>[] args(); //拦截方法对应参数.
)
04. 要拦截的类有4大类:
- Executor(update,query,flushStatements,commit,rollback,getTransaction,close,isClosed)
executor类可以说是执行sql的全过程,如组装参数,sql改造,结果处理,比较广泛
- ParameterHandler (getParameterObject,setParameters)
paremeterHandler,这个用来拦截sql的参数,可以自定义参数组装规则
- ResultSetHandler (handleResultSets,handleOutputParameters)
resultHandler,这个用来处理结果
- StatementHandler (prepare, parameterize,batch,update,query)
StatementHandler,这个是执行sql的过程,可以获取到待执行的sql,可用来改造sql,如分页,分表,最常拦截的类
Executor接口的update方法(其实也就是SqlSession的新增,删除,修改操作),所有执行executor的update方法都会被该拦截器拦截到。
⑤:MyBatis插件开发
1. 插件初始化
创建 com.it.context.interceptor.BaseInterceptor 插件类
@Intercepts(
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
)
public class BaseInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
switch (sqlCommandType) {
case INSERT:
insert(invocation.getArgs()[1]);
break;
case UPDATE:
update(invocation.getArgs()[1]);
break;
default:
break;
}
return invocation.proceed();
}
/**
* 修改操作 传递实体的话可以修改时间
* @param arg
*/
private void update(Object arg) {
if (arg instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) arg;
baseEntity.setUpdateTime(LocalDateTime.now());
}
}
/**
* 添加操作 传递实体的话可以添加时间
*
* @param arg
*/
private void insert(Object arg) {
if (arg instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) arg;
baseEntity.setCreateTime(LocalDateTime.now());
}
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
}
}
cms-portal/src/main/resources/applicationContext-datasource.xml
添加以下代码
<property name="plugins">
<array>
<bean id="baseInterceptor" class="com.it.context.interceptor.BaseInterceptor"/>
</array>
</property>
2. 测试
com.it.portal.security.filter.CmsAuthenticationFilter 类
com.it.service.dto.CmsLogDto
运行程序 完成登录
五、配置前台servlet
①:servlet-front.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"
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 http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--配置包扫描 扫描所有 但只扫描controller和controllerAdvice-->
<context:component-scan base-package="com.it.portal.controller.front" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!--引入公共的配置-->
<import resource="classpath:servlet-common.xml"/>
</beans>
②:配置web.xml
<!--springMvc配置 前台servlet-->
<servlet>
<servlet-name>frontServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:servlet-front.xml</param-value>
</init-param>
<!--servlet配置 容器启动时启动这个servlet-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>frontServlet</servlet-name>
<url-pattern>*.shtml</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>frontServlet</servlet-name>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
cms-portal/src/main/resources/applicationContext-shiro.xml
③:创建前台首页
cms-portal/src/main/webapp/WEB-INF/front/default/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
前端首页
</body>
</html>
④:创建前台控制器
创建 com.it.portal.controller.front.DynamicPageController 类
@Controller
public class DynamicPageController {
@GetMapping("index.shtml")
public String index() {
return "/front/default/index";
}
}
⑤:测试
六、搭建后台首页
①:添加资源
1.添加index.html文件
2.添加图片
- 添加css
- 添加字体库
5.添加lay-module
6.添加初始胡菜单
7.修改index.html引用文件的路径
<link rel="icon" href="${basePath}/admin/images/favicon.ico">
<link rel="stylesheet" href="${basePath}/admin/layui/css/layui.css" media="all">
<link rel="stylesheet" href="${basePath}/admin/css/layuimini.css" media="all">
<link rel="stylesheet" href="${basePath}/admin/js/font-awesome-4.7.0/css/font-awesome.min.css" media="all">
8.修改init.json文件
②:前台登录工具类
创建 com.it.context.utils.UtilsTemplate
public class UtilsTemplate {
private UtilsTemplate() {
}
/**
*后台模板方法
* @param template 模板
* @return String
*/
public static String adminTemplate(String template){
return "admin/" + template;
}
}
③:创建后台首页控制器
创建 com.it.portal.controller.admin.IndexCoontroller
@Controller
public class IndexCoontroller {
@GetMapping("index.do")
public String toIndex() {
return UtilsTemplate.adminTemplate("index");
}
}
④:登录测试
登录后会跳转到后台首页
七、线程池的应用
在我们的日常开发中,我们偶尔会遇到在业务层中我们需要同时修改多张表的数据并且需要有序的执行,如果我们用往常同步的方式,也就是单线程的方式来执行的话,可能会出现执行超时等异常,造成请求结果失败,即使成功,前端也需要等待较长时间来获取响应结果,这样不但造成了用户体验差,于是我们就需要使用线程技 术来完成我们的工作.
①:为什么不单独启动线程
为什么不单独去启动一个线程去工作?例如下面这样:
public class SimpleThread extends Thread {
)
SimpleThread simple = new SimpleThread();
simple.start();
为每个请求创建一个新线程的开销很大,为每个请求创建新线程的服务器,在创建和销毁线程上花费的时间和消耗的系统资源,要比花在处理实际的用户请求的时间和资源更多。
除了创建和销毁线程的开销之外,活动的线程也消耗系统资源。在一个JVM里创建太多的线程可能会导致系统由于过度消耗内存而用完内存或“切换过度”。为了防止资源不足,服务器应用程序需要一些办法来限制任何给定时刻处理的请求数目。
线程池为线程生命周期开销问题和资源不足问题提供了解决方案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。其好处是,因为在请求到达时线程已经存在,所以无意中也消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。而且,通过适当地调整线程池中的线程数目,也就是当请求的数目超过某个阈值时,就强制其它任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足。
②:CPU个数、内核数、线程数的区别:
- 在单核时代,单纯的CPU主频是决定CPU性能的重要指标.(CPU主频就是CPU运算时的工作频率),一般以MHz和GHz为单位.
虽然提高频率能有效提高CPU性能,但受限于制作工艺等物理因素,早在2004年,提高频率便遇到了瓶颈,于是Intel/AMD只能另辟途径来提升CPU性能,双核、多核CPU应运而生.
其实增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是1:1对应关系,也就是说四核CPU—般拥有四个线程。但Intel引入超线程技术后,使核心数与线程数形成1:2的关系,如四核Core i7支持八线程(或叫作八个逻辑核心或八个逻辑处理器),大幅提升了其多任务、多线程性能。这是在物理层面上的提高。
-
cpu个数:物理上安装了几个cpu,一般的个人电脑就装1个cpu.
-
cpu内核数: 物理上,一个cpu芯片上集成了几个内核单元,现代cpu都是多核的.
-
cpu线程数:是一种逻辑的概念,线程是调度CPU的最小单元,操作系统是通过线程来执行任务的,而每个线程可以调度一个核心.也就是说一个cpu内核可以接收并运行一个操作系统的线程.也可以通过一个CPU内核模拟出接收2个线程的CPU,也就是说,这个单核心的CPU被模拟成了一个类似双核心的CPU。我们从任务管理器的性能标签页中看到的是两个CPU。比如Inte l赛扬G460是单核心,双线程的CPU,Intel 酷睿i3 3220是双核心 四线程,Intel酷睿i7 4770K是四核心八线程,Intel酷睿i5 4570是四核心四线程等等。对于一个CPU,线程数总是大于或等于核心数的。一个核心最少对应一个线程,但通过超线程技术,一个核心可以对应两个线程,也就是说它可以同时运行两个线程.
CPU的线程数概念仅仅只针对Intel的CPU才有用,因为它是通过Intel超线程技术来实现的。如果没有超线程技术,一个CPU核心对应一个线程。所以,对于AMD的CPU来说,只有核心数的概念,没有线程数的概念。
CPU之所以要增加线程数,是源于多任务处理的需要。线程数越多,越有利于同时运行多个程序,因为线程数等同于在某个瞬间CPU能同时并行处理的任务数。因此,线程数是一种逻辑的概念.
如下图所示:插槽指cpu个数,内核数量是4个,线程数是4个.cpu不支持超线程技术,所以,一个内核只接收一个线程。
多核超线程,就是每个核有两个逻辑的处理单元,两个线程共享一个核的资源.
我不认为线程数就是核心数,你要调用cpu你必须是一个线程
推荐可以看下,<<我是一个线程>>
③:系统线程模型
线程是调度CPU的最小单元,有两种线程模型.
1.用户级线程,也叫协程:(ULT)
用户程序来实现,不依赖操作系统核心,应用控制线程的创建、同步、调度和管理;不需要用户态/内核态切换;内核对ULT无感知,线程阻塞则进程阻塞。
2.内核级线程(KLT)
切换由内核控制,当线程进行切换的时候,由用户态转化为内核态。切换完毕要从内核态返回用户态;可以很好的利用smp,即利用多核cpu.
系统内核保存线程的状态和上下文,线程的创建、调度和管理由内核完成,效率比ULT要慢一些。线程阻塞不会引起进程阻塞。在多核处理器上,多线程并行运行。要使用内核空间需要提高权限,所以要切换到内核态获取内核空间的权限,然后调用系统接口创建线程。这是比较重的操作.
JVM使用的是内核级线程.
④:cpu线程数和Java多线程:
- 线程是CPU级别的,单个线程同时只能在单个cpu线程中执行
- java多线程并不是由于cpu线程数为多个才称为多线程,当Java线程数大于cpu线程数,操作系统使用时间片机制,采用线程调度算法,频繁的进行线程切换。
- 线程是操作系统最小的调度单位,进程是资源(比如:内存)分配的最小单位
- Java中的所有线程在JVM进程中,CPU调度的是进程中的线程
举例:
可以开启的线程数和CPU核心数量是没有关系的,单核CPU也可以开启多个线程,对于单核CPU而言,多个线程看似同时执行,实际上是给每个线程分了时间片,每个线程轮流执行,只是速度很快,感觉上是“同时执行”.
开启了5个线程,但是只有4个核心的情况,四个核心在同—时刻都可以执行线程,这是真正意义上的同时执行.
⑤:线程池的应用
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
在Java中可以通过线程池来达到这样的效果,线程池的优势:
重用线程提高性能,减少线程创建消亡的开销
提高响应速度。任务到达时可以立即执行,不需要再去创建线程
方便管理线程,便于统一分配、调控和监控
我们来介绍spring中的线程池ThreadPoolTaskExecutor实现JAVA并发:
1.ThreadPoolTaskExecutor是一个spring的线程池技术,它是使用jdk中的 java.util.concurrent.ThreadPoolExecutor进行实现。
ThreadPoolTaskExecutor的参数:
-
corePoolSize:线程池维护线程的最小数量.核心线程数
-
maxPoolSize:线程池维护线程的最大数量.
-
keepAliveTime:空闲线程的存活时间.
-
threadNamePrefix:线程名前缀
-
queueCapacity:队列最大长度
-
RejectedExecutionHandler handler:用来拒绝一个任务的执行,有两种情况会发生这种情况。
-
一是在execute方法中若addlfUnderMaximumPoolSize(command)为false,即线程池已经饱和;
-
二是在execute方法中,发现runState!=RUNNING lI poolSize ==0,即已经shutdown,就调用ensureQueuedTaskHandled(Runnable command),在该方法中有可能调用reject。
-
2.ThreadPoolTaskExcutor的处理流程:
如果核心线程池未满,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
如果核心线程池已满,缓冲队列 workQueue未满,那么任务被放入缓冲队列。
如果核心线程池已满,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
如果核心线程池已满,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过handler所指定的策略来处理此任务。
3.四种拒绝策略:
ThreadPoolExecutor.AbortPolicy()抛出java.util.concurrent.RejectedExecutionException异常
ThreadPoolExecutor.CallerRunsPolicy()重试添加当前的任务,他会自动重复调用execute()方法 调用者的线程会执行该任务,如果执行器已关闭,则丢弃
ThreadPoolExecutor.DiscardOldestPolicy()抛弃旧的任务
ThreadPoolExecutor.DiscardPolicy()抛弃当前的任务
ThreadPoolExecutor.CallerRunsPolicy)的坑:
这个策略会强制中断主线程执行新任务,即是说,当我的量上来,线程池不够用的时候,中断了我的主线程,这样有可能导致很多请求接收不到。
四种策略需要结合业务的使用场景来确认使用哪个:
在cms这个项目中我们使用的是ThreadPoolExecutor.DiscardPolicy()
⑥:线程池参数如何配置?
如果是IO密集型应用,则线程池大小设置为2N+1;
如果是CPU密集型应用,则线程池大小设置为N+1;
N代表CPU的核数。
假设我的服务器是4核的,且一般进行大数据运算,cpu消耗较大,那么线程池数量设置为5为最优。
现在很多项目线程池滥用,注意分配线程数量,建议不要动态创建线程池,尽量将线程池配置在配置文件中,这样方便以后整体的把控和后期维护。每个核心业务线程池要互相独立,互不影响。
⑦:使用注解@EnableAsync和@Async来实现:
然这样实现了我们想要的结果,但是,但是我们发现如果我们在多个请求中都需要这种异步请求,每次都写这么冗余的线程池配置会不会,这种问题当然会被我们强大的spring所观察到,所以spring为了提升开发人员的开发效率,使用@EnableAsync来开启异步的支持,使用@Async来对某个方法进行异步执行.
如果在方法上添加@Async,会自动被注入使用ThreadPoolTaskExecutor作为TaskExecutor(线程池),如果配置了多个ThreadPoolTaskExecutor,可以@Async(“ThreadPoolTaskExecutor1”)来指定。
@EnableAsync: 来开启异步的支持,不用配置线程池也是可以的.
@Async: @Async所修饰的函数不要定义为static类型,这样异步调用不会生效 调用方法和异步函数不能在一个class中。
⑧:自定义线程池
推荐的方式,自定义线程池的配置类,并在类上添加@EnableAsync 注解,然后在需要异步的方法上使用@Async("线程池名称"):
1. 配置线程池
cms-portal/src/main/resources/applicationContext-configuration.xml
<!--使用spring的线程池-->
<bean id="threadPoolTaskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<!--线程池维护线程的最小数量 核心线程数-->
<property name="corePoolSize" value="5"/>
<!--线程池维护线程的最大数量-->
<property name="maxPoolSize" value="8"/>
<!--队列最大长度-->
<property name="queueCapacity" value="50"/>
<!--线程名前缀-->
<property name="threadNamePrefix" value="customThreadPoolTask"/>
<!--程池对拒绝任务(无线程可用)的处理策略-->
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$DiscardPolicy"/>
</property>
</bean>
2. 使用多线程完成记录日志
com.it.portal.security.filter.CmsAuthenticationFilter
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String requestURI = httpServletRequest.getRequestURI();
String ip = UtilsHttp.getRemoteAddress();
threadPoolTaskExecutor.execute(() -> {
CmsUserDto cmsUserDto = (CmsUserDto) subject.getPrincipal();
cmsUserDto.setLastLoginIp(ip);
cmsUserDto.setSessionId(UtilsShiro.getSession().getId().toString());
cmsUserService.update(cmsUserDto);
cmsLogService.save(CmsLogDto.of(cmsUserDto.getId(),cmsUserDto.getUsername(),ip,requestURI,"用户后台系统登录!"));
});
return false;
}
⑨: 使用logback打印日志
1. 简介
logback 继承自log4j,它建立在有十年工业经验的日志系统之上。它比其它所有的日志系统更快并且更小,包含了许多独特并且有用的特性。
ogback和log4j是一个人写的,springboot默认使用的日志框架是logback。
logback主要由logback-core:是其它模块的基础设施、其他模块基于它构建、提供了关键性的通用机制、
logback-classic:是log4j的轻量级的实现,实现了简单日志门面slf4j、
logback-access:主要作为—个与servlet容器交互的模块
配置文件结构logback.xml:
详细配置文件内容:
logback中午网站 www.docs4dev.com/docs/zh/log…
2. logback取代log4j的理由:
1.更快的实现:Logback的内核重写了,在一些关键执行路径上性能提升10倍以上。而且logback不仅性能提升了,初始化内存加载也更小了。
2.充分的测试:Logback 历经了几年,数不清小时数的测试。尽管log4j也是测试过的,但是Logback的测试更加充分,跟log4j不在同一个级别。我们认为,这正是人们选择Logback而不是log4j的最重要的原因。人们都希望即使在恶劣的条件下,你的日记框架依然稳定而可靠。
3.更好的集成:ssm中集成logback打印sql日志更方便
3. 删除log4j的包,引入logback:
<!--日志-->
<!--logback-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
cms-portal/src/main/resources/datasource.properties
4. logback.xml配置文件:
cms-portal/src/main/resources/logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoding>UTF-8</encoding>
<encoder>
<pattern>[%d{HH:mm:ss.SSS}][%p][%c{40}][%t] %m%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
</appender>
<!--文件-->
<appender name="cms" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>d:/cms.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/home/sui/log/cms.log.%d{yyyy-MM-dd}.gz</fileNamePattern>
<append>true</append>
<maxHistory>10</maxHistory>
</rollingPolicy>
<encoder>
<pattern>[%d{HH:mm:ss.SSS}][%p][%c{40}][%t] %m%n</pattern>
</encoder>
</appender>
<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>d:/error.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>d:/error.log.%d{yyyy-MM-dd}.gz</fileNamePattern>
<append>true</append>
<maxHistory>10</maxHistory>
</rollingPolicy>
<encoder>
<pattern>[%d{HH:mm:ss.SSS}][%p][%c{40}][%t] %m%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<logger name="com.cms" additivity="false" level="INFO" >
<appender-ref ref="cms" />
<appender-ref ref="console"/>
</logger>
<!-- mybatis log 日志 -->
<logger name="com.cms.dao.mapper" level="DEBUG"/>
<root level="DEBUG">
<appender-ref ref="console"/>
<appender-ref ref="error"/>
</root>
</configuration>
5. 测试
启动项目登录后可以看到日志的sql的打印
⑩:解决切换日志导致的异常
错误:Could not resolve view with name 'captcha' in servlet with name 'adminServlet'
1. 解决方法一
经过查找在调用imageCaptcha()这个方法时发生的错误
加 @ResponseBody 注解
@GetMapping("captcha.do")
@ResponseBody
public void toCaptcha() {
commonService.imageCaptcha();
}
2. 方法二
⑨:使用消息队列mq来实现:
当我们涉及的请求在业务逻辑中一次性操作很多很多的数据,例如:一个请求执行相关业务操作后,将操作日志插入到数据库中,我们可以使用@Async来实现,但是这样增加了业务和非业务关系的冗余性(同时如何并发量很大,我们使用@Async处理,无法提升我们系统的整体系统,这样很容易造成服务器宕机),所以我们对于这种情况,我们会采用mq来实现,将业务逻辑和非业务逻辑进行隔离执行,互不影响,非业务逻辑不会影响到执行业务逻辑的结果和主机性能.