文章目录
- 瑞吉外卖笔记整理
- 一、静态资源和静态资源映射
- 二、如何完成登录验证?
- 三、在开发过程中遇到端口被占用问题
- 四、关于从前端接收参数(@RequestBody、@RequestParm、直接接收、@PathVariable)
- 五、字符串拼接的新方式
- 六、Page对象
- 七、解决报错:java.sql.SQLSyntaxErrorException: Unknown column 'updateTime' in 'order clause'
- 八、由于js对long型数据的精度处理导致的问题
- 九、获取路径中的参数
- 十、公共字段自动填充
- 十一、ThreadLocal的使用
- 十二、解决报错:Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'DELETE' not supported]
- 十三、文件上传
- 十四、文件下载
- 十五、什么是DTO?为什么要用到DTO?
- 十六、事务注解
- 十七、短信发送
- 十八、解决:VMware Workstation 与 Device/Credential Guard 不兼容
瑞吉外卖笔记整理
一、静态资源和静态资源映射
什么时候需要静态资源映射呢?
前提:如果静态资源不放在static下是访问不了的
在瑞吉外卖中,一开始创建的maven文件所以在resources下就没有static这个文件夹,所以直接将静态资源放到resources下是访问不到的
对此,有两个解决方案:
- 给resources添加static这个文件夹,然后静态资源放进去就可以了
- 添加静态资源映射
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
// 添加自定义的静态资源映射路径,第一个参数是访问路径前缀,第二个参数是文件存放路径
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:static/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:static/front/");
}
}
补充知识:什么是拦截器什么是过滤器?
1.拦截器:
-
拦截器是SpringMVC框架自己的,只有使用了SpringMVC框架的工程才能使用
-
拦截器只会拦截访问的控制器方法(只能对action请求起作用),如果访问的是jsp/html/css/image/js是不会进行拦截的(即不拦截静态资源)
-
拦截器是AOP思想的具体应用。
-
拦截器可以多次被调用
-
拦截器可以访问action上下文、堆栈里的对象
-
拦截器不依赖于servlet容器
-
拦截器是基于java的反射机制的
-
拦截器可以获取IOC容器中的各个bean,在拦截器里注入一个service,可以调用业务逻辑。
-
拦截器是在DispatcherServlet这个servlet中执行的,因此所有的请求最先进入Filter,最后离开Filter
其顺序如下:
Filter- >Interceptor.preHandle-> Handler- >Interceptor.postHandle->lnterceptor.afterCompletion-> Filter
2.拦截器的应用场景
由于拦截器本质上是面向切面编程(AOP) ,符合横切关注点的功能都可以放在拦截器中来实现
主要的应用场景包括:
- 登录验证,判断用户是否登录。
- 权限验证,判断用户是否有权限访问资源,如校验token
- 日志记录,记录请求操作日志(用户ip, 访问时间等),以便统计请求访问量。
- 处理cookie. 本地化、国际化、主题等。
- 性能监控,监控请求处理时长等。
3.过滤器
- 过滤器依赖于servlet容器,是servlet规范中的一部分,任何java web工程都可以使用
- 在url-pattern中配置了/*之后,可以对所有要访问的资源进行拦截
- 过滤器只能在容器初始化时被调用一次,在action的生命周期中
- 过滤器不能访问action上下文、堆栈里的对象
- 过滤器是基于函数回调
- 过滤器不能获取IOC容器中的各个bean
二、如何完成登录验证?
两种方法:
- 使用拦截器
-
首先自定义一个LoginIntercepter继承HandlerInterceptor类
-
然后重写preHandle()方法,在preHandle中进行逻辑判断。
-
如果登录未成功则重定向到登录页面。
-
需要注意的一点是重定向要加上项目的名称,为了适应以后项目名可能会改变,所以我们要动态地获取项目名称即request.getContentText
-
在写完以后就在拦截器上写上@Conponent,这样spring容器就会自动扫描到这个组件
-
然后再WebMvcConfig配置类上注册拦截器,定义好要拦截的路径和不拦截的路径。
-
需要注意的是如果拦截的是静态资源的话我们需要手动映射一遍(resources下的static的文件可能不再被视为静态资源,最好在添加资源管理器)
2.使用全局过滤器
@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, ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//1、获取本次请求的URI
String requestURI = request.getRequestURI();// /backend/index.html
log.info("拦截到请求:{}",requestURI);
//定义不需要处理的请求路径
String[] urls = new String[]{
"/employee/login",//这个路径是点击按钮,要登录,所以要放行
"/employee/logout",
"/backend/**",
"/front/**",
"/common/**"
};
//2、判断本次请求是否需要处理
boolean check = check(urls, requestURI);
//3、如果不需要处理,则直接放行
if(check){
log.info("本次请求{}不需要处理",requestURI);
//它的作用是将请求转发给过滤器链上的下一个对象。这里的下一个指的是下一个filter,如果没有filter那就是你请求的资源。
filterChain.doFilter(request,response);
//如果符合就放行并结束,否则浪费性能
return;
}
//4、判断登录状态,如果已登录,则直接放行
Long userId=(Long) request.getSession().getAttribute(Constant.SESSION_USER);
if(userId != null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute(Constant.SESSION_USER));
BaseContext.setCurrentId(userId);
filterChain.doFilter(request,response);
return;
}
log.info("用户未登录");
//5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据,重定向由前端完成了
response.getWriter().write(JSON.toJSONString(Result.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;
}
}
三、在开发过程中遇到端口被占用问题
报错:
Tomcat的端口被占用了,我们需要修改一下端口号或把正在运行的端口号进程杀死
Description:
Web server failed to start. Port 8080 was already in use.
Action:
Identify and stop the process that's listening on port 8080 or configure this application
to listen on another port.
两种解决方案:
- 在yml文件中修改端口号为别的端口号
- 把正在运行的端口号进程杀死
- win+R —> 输入cmd —> 回车
- 输入命令 —> netstat -ano | findstr “8080”
- 找到被占用的进程号****,输入命令 —> taskkill -pid 进程号 -f
四、关于从前端接收参数(@RequestBody、@RequestParm、直接接收、@PathVariable)
问题:在写分页查询的时候会发现在controller没有用@RequestBody来接收前端传送过来的参数,而是通过直接接收,这就让我想到了@RequestParm这个注解 (当时在学的时候就没搞清楚555)
注意:从前端路径获取数据一定名字要一样
@RequestBody
主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的),
因为最常用的使用请求体传参的是POST请求,所以使用@RequestBody接收数据时,一般都用POST方式进行提交
@RequestParam
主要用来接收前端传递给后端的key-value里面的参数
注意:
如果参数前写了@RequestParam(xxx),那么前端必须有对应的xxx名字才行
前端路径同一个变量多个值传过来的时候要用@RequestParam(这点在套餐删除那里体现的很明显,但是为什么呢?)
@DeleteMapping
@CacheEvict(value = "setmealCache",allEntries = true)
public Result<?> delete(@RequestParam List<Long> ids){
setmealService.delete(ids);
return Result.success("删除成功");
}
ps:也可以不用list集合,直接一个String接过来,然后split方法用逗号分隔
直接接收
主要用来接收前端传递给后端的key-value里面的参数
注意:用来接收的参数要和前端传过来的参数名称相同
@PathVariable
从路径中获取参数
什么时候用什么:
-
如果参数时放在请求体中,application/json传入后台的话,那么后台要用@RequestBody才能接收到;(在修改分类信息的时候验证了一下,如果传过来的数据是json 就一定要用@RequestBody注解)
-
如果不是放在请求体中的话,那么后台接收前台传过来的参数时,要用@RequestParam来接收,或则形参前 什么也不写也能接收。
五、字符串拼接的新方式
以前我们都用+来进行字符串拼接,现在我们可以用大括号作为占位符
例如:
log.info("查询菜品信息,菜品id为"+id);
log.info("查询菜品信息,菜品id为{}",id);
六、Page对象
问题:为什么分页查询那里不需要搞个对象接收查询数据呢
回答:是因为查询完毕之后内部自动赋值给Page对象,即把查到的数据放到自定义的pageinfo中。(这里可以去查查Page里面有什么变量,其中有一个是records,存储的就是我们查询下出来的数据)
七、解决报错:java.sql.SQLSyntaxErrorException: Unknown column ‘updateTime’ in ‘order clause’
出现这样的问题是因为在用queryWrapper条件构造器名称与数据库中的名称不符合。
所以一般建议使用Lamda条件构造器,这样名称就不会出错了
八、由于js对long型数据的精度处理导致的问题
问题:在更新员工信息操作的时候会发现id不匹配 那是什么原因呢?
这是因为js对long型数据进行处理时丢失精度,导致提交的id和数据库中的id不一致
**解决方法:**我们可以在服务端给页面响应json数据时进行处理,将long型数据统一转为String字符串
具体实现:
-
提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换
/** * 对象映射器:基于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); } }
-
在WebMvcConfig配置类中扩展SpringMVC的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 扩展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);
}
}
简单来说就是用继承的WebMvc方法里的消息转换器然后再new出来添加自己自定义的消息转换器
九、获取路径中的参数
问题:编辑员工信息会传id过来,这个id是在路径里面的,那我们如何获取呢?
解决方法:首先给一个变量名 然后在传参那里加上注解@PathVariable 表示这是从路径获取的
十、公共字段自动填充
公共字段自动填充是mp给我们提供的一个功能,能大大的减少代码量提高效率
mp的公共字段自动填充也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码
实现步骤:
- 在实体类的属性上加上@TableFiled注解,指定自动填充策略
- 按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段开始自动填充[insert]...");
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", BaseContext.getCurrentId());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段开始自动填充[update]...");
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}
十一、ThreadLocal的使用
其实在一开始的MyMetaObjectHandler中updateUser和createUser是写死的,因为我们无法获取HttpSession对象(即无法获取session)也就没办法获取登录用户,所以只能写死。
那我们如何动态获取当前登录用户的id呢?
解决:我们可以使用ThreadLocal来解决此问题,他是JDK中提供的一个类。
不过在了解ThreadLocal之前我们需要确认:客户端发送的每次http请求对对应在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:
1). LoginCheckFilter的doFilter方法
2). EmployeeController的update方法
3). MyMetaObjectHandler的updateFill方法
我们可以在上述类的方法中加入如下代码(获取当前线程ID,并输出):
long id = Thread.currentThread().getId();
log.info("线程id为:{}",id);
执行编辑员工功能进行验证,通过观察控制台输出可以发现,一次请求对应的线程id是相同的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zhRNHiv4-1678884625502)(D:\Development\Typora\img\image-20230308234118633.png)]
什么是ThreadLocal?
ThreadLocal并不是一个Thread,而是Thread的局部变量。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问当前线程对应的值。
ThreadLocal常用方法:
- public void set(T value) : 设置当前线程的线程局部变量的值
- public T get() : 返回当前线程所对应的线程局部变量的值
- public void remove() : 删除当前线程所对应的线程局部变量的值
在该系统中ThreadLocal的应用:
我们可以在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id)
然后在MyMetaObjectHandler的updateFill方法中调用ThreadLocal的get方法来获得当前线程所对应的线程局部变量的值(用户id)
如果在后续的操作中, 我们需要在Controller / Service中要使用当前登录用户的ID, 可以直接从ThreadLocal直接获取。
注意:
以后在开发的时候自己先测试一下公共字段自动填充类和那个处理类是在同一个线程里面的 然后才能利用线程副本来进行获取操作
十二、解决报错:Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method ‘DELETE’ not supported]
删除分类时出现的错误
?
十三、文件上传
文件上传也称为upload是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程
文件上传在项目中应用非常广泛…
Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller方法中声明一个MultipartFile类型的参数即可接收上传的文件
但是这个MultipartFile类型的参数的参数名不能随便乱写 要和前端的名字对应
在瑞吉外卖中上传之后马上触发下载是为了显示在浏览器上看
@PostMapping("/upload")
public Result<?> upload(MultipartFile file){
log.info("上传文件...");
//获取原始文件名
String originalName= file.getOriginalFilename();
//获取原始文件名的后缀,从点开始包括点
String suffix=originalName.substring(originalName.lastIndexOf("."));
//随机生成新的文件名
String fileName= UUID.randomUUID().toString()+suffix;
//创建一个目录对象
File dir=new File(BasePath);
//判断目录是否存在,不存在则创建
if (!dir.exists()){
dir.mkdir();
}
try {
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
file.transferTo(new File(BasePath+fileName));
}catch (IOException e){
e.printStackTrace();
}
return Result.success(fileName);
}
十四、文件下载
文件下载也成为download是指将文件从服务器传输到本地计算机的过程
通过浏览器进行文件下载,通常有两种表现形式
- 以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
- 直接在浏览器打开
通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程
具体来说就是:
通过输入流来读取文件的内容,读取到的就放到byte数组里面
读取的停止条件是输入流读取到的内容长度不等于-1时(不等于-1说明没有读完)
然后我们是边读边写 通过这个输出流协会浏览器,在浏览器显示图片
最后还需要注意 因为我们相应回去的文件是图片 所以设置内容的类型为图片
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
try {
//通过输入流读取文件
FileInputStream inputStream=new FileInputStream(new File(BasePath+name));
//通过输出流输出显示到浏览器
ServletOutputStream outputStream=response.getOutputStream();
response.setContentType("image/ipeg");
int len=0;
byte[] bytes=new byte[1024];
while ((len=inputStream.read(bytes))!=-1){
outputStream.write(bytes);
outputStream.flush();
}
//关闭资源
inputStream.close();
outputStream.close();
}catch (IOException e){
e.printStackTrace();
}
}
十五、什么是DTO?为什么要用到DTO?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QT4bf7ng-1678884625504)(D:\Development\Typora\img\image-20230315195601275.png)]
如图,在添加菜品的时候的数据比较复杂 里面还有一个实体类的数据 所以我们在controller写参数的时候就不能简单地写其中一个实体类。
那要怎么解决这些问题呢?
答:写一个实体类封装我们页面提交的数据
DTO,全称为 Data Transfer Object,即数据传输对象,一般用于展示层与服务层之间的数据传输
其实就是当我们定义的实体类不能满足我们的开发时,可以自己封装对应的数据对象来满足数据传输要求
在瑞吉外卖中用到DTO的还有菜品的分页查询:
首先菜品的分类查询在页面上显示的是菜品分类的名称 ,但是在Dish表中菜品分类存储的是id,所以我们在查询到数据后传送过去的是id,所以菜品分类不会正常显示
显然,分页查询范围Dish这种类型的数据是不对的,所以就可以用到前面提到的DTO
具体的处理就是:
-
将原先的分页查询对象(dish)复制到新的分页查询对象(dishDto)中,复制的时候不复制records。
因为Page里面的record就是查出来的所有数据,而我们不想要第一个Dish查出来的(因为里面有一个不符合页面要求的数据) 所以就不拷贝这个数据。
-
将原先Page对象的records查出来的所有不带菜品分类名称的dish取出来处理一下加上菜品分类,再注入到DishDto所构造的分页查询中
-
最后返回新的分页查询对象
@GetMapping("/page")
public Result<Page<DishDto>> page(int page,int pageSize,String name){
log.info("分页查询:page-{} pageSize-{}",page,pageSize);
//构造分页构造器
Page<Dish> pageinfo=new Page<>(page,pageSize);
Page<DishDto> dishPageinfo=new Page<>(page,pageSize);
//构造条件构造器
QueryWrapper queryWrapper=new QueryWrapper<>();
//过滤
//首先要判断名字不为空才过滤
queryWrapper.like(StringUtils.isNotEmpty(name),"name",name);
//排序
queryWrapper.orderByDesc("update_time");
//查询
dishService.page(pageinfo,queryWrapper);
//对象拷贝,除了records,因为要先把categoryid处理成name,再放到dto的pageinfo中(把前者复制到后者,除了records)
BeanUtils.copyProperties(pageinfo,dishPageinfo,"records");
//把records取出来进行处理
List<Dish> records=pageinfo.getRecords();
List<DishDto> newRecords=new ArrayList<>();
for (Dish dish: records) {
//先取出id
Long categoryId=dish.getCategoryId();
//然后根据这个id查出name
Category category=categoryService.getById(categoryId);
String categoryName=category.getName();
//取出来之后放到DTO对象去,原来的数据也要拷贝过去
DishDto dishDto=new DishDto();
dishDto.setCategoryName(categoryName);
BeanUtils.copyProperties(dish,dishDto);
//最后把这些DTO对象放到list中
newRecords.add(dishDto);
}
//把records处理好之后,放到那个DTO的分页构造器上
dishPageinfo.setRecords(newRecords);
return Result.success(dishPageinfo);
}
十六、事务注解
当一个方法设计多张表的操作,例如在添加菜品的时候还有设计添加口味所以要在该方法上添加事务注解
@Transactional
public void saveWithFlavor(DishDto dishDto){
//保存菜品的基本信息到菜品表
this.save(dishDto);
//保存口味到口味表
//但是由于口味只有name和value有值 所以缺少了菜品的id,因此我们应该添加上此id
Long dishId=dishDto.getId();
List<DishFlavor> list=dishDto.getFlavors();
for (DishFlavor dishFlavor: list) {
dishFlavor.setDishId(dishId);
}
dishFlavorService.saveBatch(list);
}
然后为了让该注解起作用还要在启动类上添加@EnableTransactionManagement
十七、短信发送
- 阿里云官网注册账号
- 注册成功后,登录。
- 登录后进入短信服务管理页面,选择国内消息菜单,设置短信签名(短信签名是短信发送者的署名,表示发送方的身份)
- 代码开发:使用阿里云短信服务发送短信,可以参考官方提供的文档
- 导入maven坐标
<!--阿里云短信服务-->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.16</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.1.0</version>
</dependency>
- 调用API
package com.itheima.reggie.utils;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
/**
* 短信发送工具类
*/
public class SMSUtils {
/**
* 发送短信
* @param signName 签名
* @param templateCode 模板
* @param phoneNumbers 手机号
* @param param 参数
*/
public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");
IAcsClient client = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setSysRegionId("cn-hangzhou");
request.setPhoneNumbers(phoneNumbers);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam("{\"code\":\""+param+"\"}");
try {
SendSmsResponse response = client.getAcsResponse(request);
System.out.println("短信发送成功");
}catch (ClientException e) {
e.printStackTrace();
}
}
}
- 生成验证码
package com.itheima.reggie.utils;
import java.util.Random;
/**
* 随机生成验证码工具类
*/
public class ValidateCodeUtils {
/**
* 随机生成验证码
* @param length 长度为4位或者6位
* @return
*/
public static Integer generateValidateCode(int length){
Integer code =null;
if(length == 4){
code = new Random().nextInt(9999);//生成随机数,最大为9999
if(code < 1000){
code = code + 1000;//保证随机数为4位数字
}
}else if(length == 6){
code = new Random().nextInt(999999);//生成随机数,最大为999999
if(code < 100000){
code = code + 100000;//保证随机数为6位数字
}
}else{
throw new RuntimeException("只能生成4位或6位数字验证码");
}
return code;
}
/**
* 随机生成指定长度字符串验证码
* @param length 长度
* @return
*/
public static String generateValidateCode4String(int length){
Random rdm = new Random();
String hash1 = Integer.toHexString(rdm.nextInt());
String capstr = hash1.substring(0, length);
return capstr;
}
}
手机验证码登录的优点:
- 方便快捷、无须注册、直接登录
- 使用短信验证码作为登录凭证,无需记忆密码
- 安全
登录流程:
输入手机号->获取验证码->输入验证码->点击登录->登陆成功
注意:通过手机验证码登录,手机号是区分不同用户的标识
十八、解决:VMware Workstation 与 Device/Credential Guard 不兼容
为什么出现这个问题?
因为在官网下载了win版的docker,而会自带下载虚拟机Hyper-V,这个和之前下载的vmware虚拟机造成冲突了,导致后者不能使用,所以打开vmware报错如下:
VMware Workstation 与 Device/Credential Guard 不兼容。在禁用 Device/Credential Guard 后,可以运行 VMware Workstation。
解决方法一:
关闭hyper-v:
控制面板—程序和功能 —启用或者关闭windows功能—勾选或者去掉够远的Hyper-v,然后重启
但是这个方法对我没用
解决方法二:
禁用Device Guard:
- [win+R] 打开输入 gpedit.msc ,依次 本地计算机策略 --> 计算机配置 --> 管理模板>系统 --> Device Guard,将 基于虚拟化的安全设置 设置为 “已禁用”
但是这个方法我遇到了一个问题:找不到gpedit.msc文件
如果没有该文件,表明未安装组策略编辑器。
@echo off
pushd "%~dp0"
dir /b C:\Windows\servicing\Packages\Microsoft-Windows-GroupPolicy-ClientExtensions-Package~3*.mum >List.txt
dir /b C:\Windows\servicing\Packages\Microsoft-Windows-GroupPolicy-ClientTools-Package~3*.mum >>List.txt
for /f %%i in ('findstr /i . List.txt 2^>nul') do dism /online /norestart /add-package:"C:\Windows\servicing\Packages\%%i"
pause
将这段代码保存到一个空的text文件,保存文本文档后缀改为**.bat**格式,以管理员身份运行该系统文件。
这样就会有gpedit.msc文件
- 按下“win+R”打开运行,输入 gpedit.msc 即可打开组策略编辑器。
解决方法三:(真的解决了问题)
- cmd命令输入regedit进入注册表,找到HKEY_LOCAL_MACHINE \ System \ CurrentControlSet \ Control \ DeviceGuard,将默认的值改为0
- 【win+x】选择 Windows Powershell (管理员)(A) 执行 bcdedit /set hypervisorlaunchtype off
- 重启电脑。