(1)浏览器地址栏网址使用 斜杆/ ;
(2)windows文件浏览器上使用 反斜杠\ ;
(3)出现在html url() 属性中的路径,指定的路径是网络路径,所以必须用 斜杆/ ;
-
stream
在stream流在处理过程中看做是在副本进行操作并没有改变源数据,进行Collection.toList()终止操作。 -
MybatisPlus中@TableField注解的使用
值 | 描述 |
---|---|
value | 字段值(驼峰命名方式,该值可无),使用时在全局策略配置中关闭驼峰命名 |
update | 预处理set字段自定义注入 update=“%s+1(xxx)”,%s会填充为字段(字段=xxx) |
condition | 预处理WHERE实体条件自定义运算规则 |
el | 详看注释说明 |
exist | 是否为数据库表字段(默认为true存在) |
strategy | 字段验证(默认非null判断) |
fill | 字段填充标记(FieldFill配合自动填充使用) |
- 字段填充策略FieldFill
值 | 描述 |
---|---|
DEFAULT | 默认不处理 |
INSERT | 插入填充字段 |
UPDATE | 更新填充字段 |
INSERT_UPDATE | 插入和更新填充字段 |
- 注解和方法相关
1、 request是需要接收来自浏览器的请求数据
2、@RequestBody是需要将前端的json数据解析成实体对象存储
3、处理时间用Date,但是。。。;可以用SimpleDateFormat格式化日期,但是它是线程不安全的;用LocalDateTime是线程安全的。LocalDateTime.now()获取当前时间。
4、 @ControllerAdvice全局异常处理器的注解,通过属性annotations指定拦截的Controller方法
5、 @ExceptionHandler来指定拦截的是哪一类型的异常。括号里填异常名字的字节码文件。
6、@ResponseBody将方法的返回值 R 对象转换为json格式的数据, 响应给页面;
项目结构
- config:配置类
- dao(mapper):数据层
- controller:表现层
- service:业务层
- entity(domain):实体类
- common:通用结果类,服务端响应的所有结果最终都会包装成此种类型返回给前端页面
消息转换器
SpringMVC流程
mybatisx
工程配置类
WebMvcConfig
MyBatisPlusConfig
工程配置文件
#指定端口号
server:
port: 8080
spring:
application:
#应用名称 , 可选
name: reggie_take_out
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
mybatis-plus:
configuration:
#在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射 address_book ---> AddressBook
map-underscore-to-camel-case: true
#日志输出
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
BaseMapper接口
- 继承该接口后,无需编写mapper.xml文件就可以获得CRUD功能。
- 记得加上@Mapper注解给Spring进行管理
IService接口
IService的使用需要另外两个接口的配合:BaseMapper和ServiceImpl。
IService 依赖于 Spring 容器,而 BaseMapper 不依赖;BaseMapper
可以继承并添加新的数据库操作,IService 要扩展的话还是得调用 Mapper,
- 第一步实现BaseMapper接口
- 第二步编写service类接口继承IService
- 第三步编写serviceImpl类,是各种方法的实现
- 继承ServiceImpl类,两个泛型,第一是继承BaseMapper的Mapper类,第二个是实体类。
- 第四步使用
启动类
通过引导类启动该项目
@SpringBootApplication:是Sprnig Boot项目的核心注解,目的是开启自动配置。
@ServletComponentScan:自动扫描项目中(当前包及其子包下)的@WebServlet,@WebFilter,@WebListener注解,自动注册Servlet相关组件。
@Slf4j
@SpringBootApplication
@ServletComponentScan
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class,args);
log.info("项目启动成功...");
}
}
前端静态资源映射
SpringBoot中规定了某些包路径下的资源是静态资源, 默认静态资源的存放目录为 : “classpath:/resources/”, “classpath:/static/”, “classpath:/public/” ; 在默认的情况下,这个包下的静态资源是可以直接访问的:
- static
- public
- resources
- META-INF/resources
当动态资源请求路径跟我们的静态资源路径名称相同,SpringMVC会走动态资源映射规则,因为当一个请求发送过来时,SpringMVC的视图解析器会先去匹配动态资源,如果匹配不到,再去匹配静态资源。如果都匹配不到,就会报404。
访问非默认目录中静态资源
创建配置类WebMvcConfig
@Slf4j//日志
@Configuration//定义其为配置类
//继承WebMvcConfigurationSupport去设置静态资源映射,重写addResourceHandlers方法
//@EnableWebMvc等同于@Configuration+继承WebMvcConfigurationSupport
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源映射
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射...");
//addResourceHandler映射访问的url,addResourceLocations映射resources下的目录
//Loctions最后一个要加/因为前面的Handler路径要加到Loction中完成整个路径的查找 registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
}
公共字段自动填充
修改数据库表字段时需要设置创建时间、创建人、修改时间、修改人等字段;在编辑员工时需要修改时间、修改人等字段。这些字段属于公共字段,需要手动赋值。考虑到涉及代码较多且冗余,使用MybatisPlus提供的公共字段自动填充功能。
字段名 | 赋值时机 | 说明 |
---|---|---|
createTime | 插入(INSERT) | 当前时间 |
updateTime | 插入(INSERT) , 更新(UPDATE) | 当前时间 |
createUser | 插入(INSERT) | 当前登录用户ID |
updateUser | 插入(INSERT) , 更新(UPDATE) | 当前登录用户ID |
- 在实体类的属性上加入@TableField注解,指定自动填充策略。
- 安装框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口。
- 在实现类中不能直接获得HttpSession对象
/**
* 自定义元数据对象处理器
*/
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
/**
* 插入操作,自动填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充[insert]...");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime",LocalDateTime.now());
//metaObject.setValue("createUser",new Long(1));
//metaObject.setValue("updateUser",new Long(1));
metaObject.setValue("createUser",BaseContext.getCurrentId());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
}
/**
* 更新操作,自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[update]...");
log.info(metaObject.toString());
metaObject.setValue("updateTime",LocalDateTime.now());
//metaObject.setValue("updateUser",new Long(1));
metaObject.setValue("updateUser",BaseContext.getCurrentId());
}
}
ThreadLocal获取用户id
自动填充并不能获取到当前登录用户通过HttpSession。但是线程id和当前登录员工id一样。此时用到ThreadLocal。
- 编写BaseContext工具类,基于ThreadLocal封装的工具类。
- 在LoginCheckFilter的doFilter方法中获取当前登录用户id(调用BaseContext),并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id)。
- 在自动填充实现类MyMetaObjectHandler的方法中调用BaseContext获取登录用户的id。
/**
* 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
*/
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
/**
* 设置值
* @param id
*/
public static void setCurrentId(Long id){
threadLocal.set(id);
}
/**
* 获取值
* @return
*/
public static Long getCurrentId(){
return threadLocal.get();
}
}
登录功能
1、将页面提交的密码password进行md5加密处理,得到加密后的字符串。
2、根据页面提交的用户名username查询数据库中的员工数据信息。
3、如果没有查询到,则返回登录失败结果。
4、查询到后进行密码对比,如果不一致,则返回登录失败结果。
5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果。
6、登录成功,将员工id存入session(@RequestBody),并返回登录成功结果。
- 前端发起的请求应为post请求
- 前端传递的请求参数为json数据,使用对象接收,故需要在形参前加注解@RequestBody
/**
* 员工登录
* @param request
* @param employee
* @return
*/
@PostMapping("/login")
//request是需要接收来自浏览器的请求数据,@RequestBody是需要将前端的json数据解析成Employee对象存储
public R<Employee> login(HttpServletRequest request,@RequestBody Employee employee){
//1、将页面提交的密码password进行md5加密处理
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());
//2、根据页面提交的用户名username查询数据库
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
//eq(Employee::getUsername,employee.getUsername())意思是数据库中的username和浏览器中username是否相等
queryWrapper.eq(Employee::getUsername,employee.getUsername());
Employee emp = employeeService.getOne(queryWrapper);
//3、如果没有查询到则返回登录失败结果
if(emp == null){
return R.error("登录失败");
}
//4、密码比对,如果不一致则返回登录失败结果
if(!emp.getPassword().equals(password)){
return R.error("登录失败");
}
//5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
if(emp.getStatus() == 0){
return R.error("账号已禁用");
}
//6、登录成功,将员工id存入Session并返回登录成功结果
request.getSession().setAttribute("employee",emp.getId());
return R.success(emp);
}
- 浏览器访问Tomcat,Tomcat首先会根据用户的url,在第一次使用Servlet的时候创建Servlet,并在这个时候创建request和response,request对象中封装请求数据
- Tomcat将request和response两个对象传给对应Servlet的service方法
- 程序员通过操作request拿到浏览器的请求数据,然后将响应消息封装到response
- 服务器在给浏览器回应之前,从response中拿到响应消息
MP的条件构造器之LambdaQueryWrapper
-
:: 在java8中的作用就是获得方法
-
Wrapper : 条件构造抽象类,最顶端父类,抽象类中提供4个方法西面贴源码展示
-
AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
-
AbstractLambdaWrapper : Lambda 语法使用Wrapper统一处理解析 lambda 获取 column。
-
LambdaQueryWrapper :看名称也能明白就是用于Lambda语法使用的查询Wrapper
-
LambdaUpdateWrapper : Lambda 更新封装Wrapper
-
QueryWrapper : Entity 对象封装操作类,不是用lambda语法
-
UpdateWrapper : Update 条件封装,用于Entity对象更新操作
登录功能-过滤器
1、获取本次请求的URI
当前访问网址的url;
2、判断本次请求是否需要处理
3、如果不需要处理,则直接放行
4、判断登录状态,已登录直接放行
5、未登录则返回未登录结果,向客户端响应数据(输出流方式)
- @WebFilter注解,filterName指定过滤器的名字 urlPatterns指定过滤器的url格式
- @ServletComponentScan作用:在SpringBoot项目中, 在引导类/配置类上加了该注解后, 会自动扫描项目中(当前包及其子包下)的@WebServlet , @WebFilter , @WebListener 注解, 自动注册Servlet的相关组件 ;
/**
* 检查用户是否已经完成登录
*/
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter{
//路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//1、获取本次请求的URI.因为到得到相对路径backend/index.html
String requestURI = request.getRequestURI();// /backend/index.html
log.info("拦截到请求:{}",requestURI);
//定义不需要处理的请求路径
String[] urls = new String[]{
"/employee/login",//登录页面本身
"/employee/logout",//退出后
"/backend/**",//某些静态页面
"/front/**"//某些静态页面
};
//2、判断本次请求是否需要处理
boolean check = check(urls, requestURI);
//3、如果不需要处理,则直接放行
if(check){
log.info("本次请求{}不需要处理",requestURI);
filterChain.doFilter(request,response);
return;
}
//4、判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee") != null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
// filterChain.doFilter(request,response);
//从request中获取用户id保存到ThreadLocal中
Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);
return;
}
log.info("用户未登录");
//5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
/**
* 路径匹配,检查本次请求是否需要放行
* @param urls
* @param requestURI
* @return
*/
public boolean check(String[] urls,String requestURI){
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestURI);
if(match){
return true;
}
}
return false;
}
}
request里的信息
方法 | 说明 |
---|---|
request.getScheme() | 返回的协议名称,默认是http |
request.getServerName() | 返回的是浏览器中显示的主机名 |
request.getServerPort() | 获取服务器端口号 |
request.getRealPath(“url”); | 虚拟目录映射为实际目录 |
request.getRealPath(“./”); | 网页所在的目录 |
request.getRealPath(“…/”); | 网页所在目录的上一层目录 |
request.getContextPath(); | 应用的web目录的名称 |
request.getRequestURI() | 包含工程名的当前页面全路径(相对地址) |
request.getRequestURL() | 得到IE地址栏地址 |
AntPathMatcher路径匹配
Spring设计了PathMatcher路径匹配器接口,用于支持带通配符的资源路径匹配。这个接口的应用:
name | description |
---|---|
PathMatchingResourcePatternResolver | 资源扫描,启动时扫描并加载资源 |
AbstractUrlHandlerMapping | 请求路径映射到Controller |
WebContentInterceptor | 拦截器拦截路径分析 |
作用: 在项目中主要用来做路径的匹配,在权限模块会用到接口路径的匹配。
默认路径分隔符为“/”。Windows下为“\”,Linux下为“/”。
ant匹配规则
字符 | 描述 |
---|---|
? | 匹配一个字符 |
* | 匹配0个及以上字符 |
** | 匹配0个及以上目录 |
退出功能
接收页面依旧是发送post请求
/**
* 员工退出
* @param request
* @return
*/
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
//清理Session中保存的当前登录员工的id
request.getSession().removeAttribute("employee");
return R.success("退出成功");
}
新增员工功能
- 点击保存按钮,页面发送AJAX请求,将新增元页面中输入的数据以json的形式提交到服务端,请求方式为POST,请求路径为/employee
- 服务端Controller接收页面提交的数据并调用Service将数据进行保存(Controller中的save方法)
- 新增员工时,需要给员工设置初始默认密码,并进行加密
- 组装员工信息时,需要封装创建时间、修改时间、创建人、修改人信息(从session中获取当前登录用户)
- Service调用Mapper操作数据库,保存数据
/**
* 新增员工
* @param employee
* @return
*/
@PostMapping
public R<String> save(HttpServletRequest request,@RequestBody Employee employee){
log.info("新增员工,员工信息:{}",employee.toString());
//设置初始密码123456,需要进行md5加密处理
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
// employee.setCreateTime(LocalDateTime.now());
// employee.setUpdateTime(LocalDateTime.now());
//获得当前登录用户的id
// Long empId = (Long) request.getSession().getAttribute("employee");
// employee.setCreateUser(empId);
// employee.setUpdateUser(empId);
employeeService.save(employee);
return R.success("新增员工成功");
}
全局异常处理
前端用户操作不符合数据库某些字段规则时会报错但保存信息并不详细。可以用trycatch方法,当更建议用一个通用的全局异常处理器解决。
全局异常处理器
在项目组自定义一个全局异常处理器,在异常处理器上加上注解 @ControllerAdvice,可以通过属性annotations指定拦截哪一类的Controller方法。 并在异常处理器的方法上加上注解 。@ExceptionHandler 来指定拦截的是那一类型的异常。
@ControllerAdvice : 指定拦截那些类型的控制器;
@ResponseBody: 将方法的返回值 R 对象转换为json格式的数据, 响应给页面;
以上=@RestControllerAdvice
/**
* 全局异常处理
*/
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 异常处理方法
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
if(ex.getMessage().contains("Duplicate entry")){
String[] split = ex.getMessage().split(" ");
String msg = split[2] + "已存在";
return R.error(msg);
}
return R.error("未知错误");
}
}
员工分页查询
请求参数:搜索条件-员工姓名(模糊查询)、分页条件-每页展示条数、页码。
响应数据:总记录数、结果列表
/**
* 员工信息分页查询
* @param page 当前查询页码
* @param pageSize 每页展示记录数
* @param name 员工姓名 - 可选参数
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
log.info("page = {},pageSize = {},name = {}" ,page,pageSize,name);
//构造分页构造器
Page pageInfo = new Page(page,pageSize);
//构造条件构造器
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();
//添加过滤条件
queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
//添加排序条件
queryWrapper.orderByDesc(Employee::getUpdateTime);
//执行查询
employeeService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
分页插件-分页构造器
在MybatisPlus中要实现分页功能,需要用到MybatisPlus中提供的分页插件,故要在配置类中声明分页插件的bean对象,故写一个MybatisPlus的配置类。
@Configuration
public class MybatisPlusConfig{
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor=new MybatisPlusInterceptor ();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
启用/禁用员工账号
本质上是一个更新的操作
/**
* 根据id修改员工信息
* @param employee
* @return
*/
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
log.info(employee.toString());
//Long empId = (Long)request.getSession().getAttribute("employee");
//employee.setUpdateTime(LocalDateTime.now());
//employee.setUpdateUser(empId);
employeeService.updateById(employee);
return R.success("员工信息修改成功");
}
-
控制台输出的SQL发现页面传递过来的员工id值和数据库中的id值不一致。因为服务端会将返回的R对象进行json序列化,转换为json格式的数据。在前端JS中,js对长度较长的长整型数据进行处理时,会损失精度。
-
解决:只需让js处理的ID数据类型为字符串类型即可。
- 在SpringMVC中,将Controller方法返回值转换成json对象,是通过jackson实现的。涉及到SpringMVC中的一个消息转换器MappingJackson2HttpMessageConverter
-
提供对象转换器JacksonObjectMapper,消息转换器在WebMvcConfig中配置。
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
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_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(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.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);
}
}
/**
* 扩展mvc框架的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
converters.add(0,messageConverter);
}
编辑员工信息
- 根据id查询指定修改员工信息
- 保存修改
/**
* 根据id查询员工信息
* @param id
* @return
*/
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id){
log.info("根据id查询员工信息...");
Employee employee = employeeService.getById(id);
if(employee != null){
return R.success(employee);
}
return R.error("没有查询到对应员工信息");
}
/**
* 根据id修改员工信息
* @param employee
* @return
*/
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
log.info(employee.toString());
Long empId = (Long)request.getSession().getAttribute("employee");
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(empId);
employeeService.updateById(employee);
return R.success("员工信息修改成功");
}
删除员工信息
- 根据id查询
- 数据回显
/**
* 删除员工
* @param employee
* @return
*/
@DeleteMapping
public R<String> delete(@RequestBody Employee employee){
log.info("删除员工,员工信息:{}",employee.toString());
employeeService.removeById(employee.getId());
return R.success("删除员工成功");
}