能力说明:
掌握封装、继承和多态设计Java类的方法,能够设计较复杂的Java类结构;能够使用泛型与集合的概念与方法,创建泛型类,使用ArrayList,TreeSet,TreeMap等对象掌握Java I/O原理从控制台读取和写入数据,能够使用BufferedReader,BufferedWriter文件创建输出、输入对象。
暂时未有相关云产品技术能力~
用户登录是网站基本上最常见的功能了。当然,使用Spring Boot实现用户登录也不是难事,今天我将分享,从零开始制作一个用户登录功能的过程。当然,大家还是需要掌握Spring Boot基本使用以及MyBatis的配置(SSM基本操作),再来看这篇文章比较好。在最开头我们先约定:主类所在的项目包路径:com.example.userlogin,后面其余新建的软件包都在这个包路径之下。文章最末尾我也会给出示例的仓库地址。1,基本知识-用户登录的实现原理对于用户登录,我们一定都听说过cookie这个词。其实,cookie是网络编程中使用最广泛的技术之一,它用于储存用户的登录信息。它储存在用户本地,每次随着网络请求发送给服务端,服务端就用这个判断用户是否登录。可以看看这个图:可见,用户未登录之前,http请求是不带cookie的,登录后,客户端会将登录信息放在请求中给服务端,服务端进行验证,登录成功后,服务端会把用户信息放在cookie里面,随着响应返回给客户端,客户端就会储存这个cookie。下次再访问网站,cookie会和客户端的请求一起发给服务端,服务端验证信息正确,判断这个用户是登录状态。cookie里面也是以key-value形式储存数据,并且cookie有它自己的属性例如生命周期、生效域名等等。但是实际上,cookie里面由于存放着用户信息例如用户名密码等等,很容易存在安全隐患,cookie可以被拦截甚至伪造。因此现在网站登录都使用session机制,它和cookie机制最大的区别就是用户信息不再放在客户端而是服务端,交互过程如图:可见session机制就是以cookie作为载体,cookie里面只储存一个session id,与服务端通信。每个客户端每一次登录请求都会和服务端会生成唯一的session id,相当于客户端每次只要告诉服务端session id,服务端就可以找到相对应的客户端数据。至于session id是什么,其实也很好理解。session id用于标识某个电脑和服务端这一次登录的会话,也就是说在服务端看来,一个session id对应着一台电脑,并且它是唯一的。首先登录的时候,服务端验证用户名密码正确,就生成一个唯一的session id,并把这个用户信息存在服务端里面,这个信息就对应着这个session id。与此同时这个session id就放在cookie里面发给客户端保存。下次客户端访问服务端,就带着这个cookie,服务端利用里面的session id在服务端找到对应的用户信息即可进行验证。因此下面示例我们都使用session机制进行。使用Spring Boot可以很轻松的实现session读写。2,开始-进行配置我们项目的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 https://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>3.0.4</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>user-login</artifactId> <version>1.0.0</version> <name>UserLogin</name> <description>UserLogin</description> <properties> <java.version>17</java.version> <maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.target>${java.version}</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- Jackson - Json注解 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.12.4</version> </dependency> <!-- codec - 加密 --> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> </dependency> <!-- commons-lang3 - 实用工具 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <!-- Spring Session --> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-core</artifactId> </dependency> <!-- Spring Validation - 校验工具 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- Spring Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis - 数据库框架 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.1</version> </dependency> <!-- MySQL连接驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.32</version> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Spring Test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>主要是看看依赖部分需要用到什么,依赖可以根据自己实际情况进行增删,这里我主要用这些,大多数在创建Spring Boot工程时也可以进行选择。然后配置项目配置文件application.properties# 网站端口配置 server.port=8802 # JSON配置,设定不对未知字段和空值字段进行序列化节省流量 spring.jackson.deserialization.fail-on-unknown-properties=false spring.jackson.default-property-inclusion=non_null # MySQL数据库地址和账户配置(根据自己实际情况进行填写) spring.datasource.url=jdbc:mysql://localhost:3306/miyakogame?serverTimezone=GMT%2B8 spring.datasource.username=swsk33 spring.datasource.password=dev-2333对于Mybatis的Mapper XML文件,默认位于src/main/resources/com/{xxx}/{xxx}/dao/之下,即和你的dao包位置对应。例如我们项目的Mapper类一般会放在com.example.userlogin.dao下,那么Spring Boot就默认去这个地方扫描Mapper XML:src/main/resources/com/example/userlogin/dao,目录需手动创建。当然我们也可以进行指定:mybatis.mapper-locations=file:Resources/mybatisMapper/*.xml这里我们的项目就不配置MyBatis的XML位置了,就使用默认位置。配置全部根据自己实际情况进行填写。3,封装一个请求结果类Result<T>为了方便起见,我们通常封装一个结果类,里面主要是请求结果代码、消息、是否操作成功和数据体,可以根据自己需要进行修改。新建软件包model,并在其中新建Result<T>内容如下:package com.example.userlogin.model; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import java.io.Serializable; /** * 请求结果类 * * @param <T> 数据体类型 */ @Setter @Getter @NoArgsConstructor public class Result<T> implements Serializable { /** * 消息 */ private String message; /** * 是否操作成功 */ private boolean success; /** * 返回的数据主体(返回的内容) */ private T data; /** * 设定结果为成功 * * @param msg 消息 */ public void setResultSuccess(String msg) { this.message = msg; this.success = true; this.data = null; } /** * 设定结果为成功 * * @param msg 消息 * @param data 数据体 */ public void setResultSuccess(String msg, T data) { this.message = msg; this.success = true; this.data = data; } /** * 设定结果为失败 * * @param msg 消息 */ public void setResultFailed(String msg) { this.message = msg; this.success = false; this.data = null; } }一般都会返回给前端这个结果对象,更加便捷的传递是否成功以及操作消息等等。注意前后端传递交互的数据都需要实现序列化接口并且要有无参构造器,这里使用了Lombok的注解,下面也一样。4,建立用户类,并初始化数据库表创建软件包dataobject,并在其中建立用户类User,实际根据业务需要不同,用户类可能有很多属性,这里我们只建立最简单的如下:package com.example.userlogin.dataobject; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import java.io.Serializable; /** * 用户类 */ @Setter @Getter @NoArgsConstructor @JsonIgnoreProperties(value = {"password"}, allowSetters = true) public class User implements Serializable { /** * 用户id */ private Integer id; /** * 用户名 */ @NotEmpty(message = "用户名不能为空!") private String username; /** * 密码 */ @NotEmpty(message = "密码不能为空!") @Size(min = 8, message = "密码长度不能小于8!") private String password; }我们设定了用户的最基本属性,并对其设定了校验规则(不熟悉Spring Validation可以看看这篇文章),密码为敏感信息,因此我们使用Jackson注解设定密码字段为不允许序列化(不会传给前端)。创建了用户类,我们数据库表也需要对应起来,初始化一个用户表,我这里sql如下:drop table if exists `user`; create table `user` ( `id` int unsigned auto_increment, `username` varchar(16) not null, `password` varchar(32) not null, primary key (`id`) ) engine = InnoDB default charset = utf8mb4;连接MySQL并use相应数据库,将这个sql文件使用source命令执行即可。每个对象都有主键id,且一般设为自增无符号整数,这样有最高的数据库读写效率。密码一般使用MD5加密储存,因此固定32位长度。一般每个数据库表都有创建时间gmt_created和修改时间gmt_modified字段,这里简单起见省略。5,创建数据服务层-DAO并配置Mapper XMLDAO层主要是Java的对于数据库操作的接口和实现类。MyBatis的强大之处,就是只需定义接口,就可以实现操作数据库。创建软件包dao,新建接口UserDAO:package com.example.userlogin.dao; import com.example.userlogin.dataobject.User; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserDAO { /** * 新增用户 * * @param user 用户对象 * @return 新增成功记录条数 */ int add(User user); /** * 修改用户信息 * * @param user 用户对象 * @return 修改成功记录条数 */ int update(User user); /** * 根据id获取用户 * * @param id 用户id * @return 用户对象 */ User getById(Integer id); /** * 根据用户名获取用户 * * @param username 用户名 * @return 用户对象 */ User getByUsername(String username); }根据实际需要定义数据库增删改查方法,这里就定义这些。注意这里接口上面要打上@Mapper注解表示它是个数据持久层接口。且一般来说,增删改方法的返回值都是int,表示操作成功记录条数,查方法一般是返回相应对象或者对象的List。然后编写Mapper XML文件,我们在项目文件夹下的src/main/resources目录下创建多级目录:com/example/userlogin/dao,在这个目录下存放XML文件。创建UserDAO.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.example.userlogin.dao.UserDAO"> <resultMap id="userResultMap" type="com.example.userlogin.dataobject.User"> <id column="id" property="id"/> <result column="username" property="username"/> <result column="password" property="password"/> </resultMap> <insert id="add" parameterType="com.example.userlogin.dataobject.User"> insert into `user` (username, password) values (#{username}, #{password}) </insert> <update id="update" parameterType="com.example.userlogin.dataobject.User"> update `user` set password=#{password} where id = #{id} </update> <select id="getById" resultMap="userResultMap"> select * from `user` where id = #{id} </select> <select id="getByUsername" resultMap="userResultMap"> select * from `user` where username = #{username} </select> </mapper>这样,数据库操作层就完成了!一般约定某一个对象(xxx)的数据库操作层接口一般命名为xxxDAO(有的企业也命名为xxxMapper),一个xxxDAO接口对应一个xxxDAO.xml文件。6,创建用户服务层现在就要进行正式的服务层逻辑了,完成我们的主要功能:用户注册、登录、信息修改。其实,用户注册就是前端发送用户注册信息(封装为User对象),后端检验然后往数据库增加一条用户记录的过程;登录也是前端发送用户登录信息,同样封装为User对象,后端根据这个对象的username字段从数据库取出用户、进行比对最后设定session的过程;修改用户也是前端发送修改后的用户信息的User对象,后端进行比对,然后修改数据库相应记录的过程。先新建包service,在其中添加用户服务接口UserService:package com.example.userlogin.service; import com.example.userlogin.dataobject.User; import com.example.userlogin.model.Result; import jakarta.servlet.http.HttpSession; import org.springframework.stereotype.Service; @Service public interface UserService { /** * 用户注册 * * @param user 用户对象 * @return 注册结果 */ Result<User> register(User user); /** * 用户登录 * * @param user 用户对象 * @return 登录结果 */ Result<User> login(User user); /** * 修改用户信息 * * @param user 用户对象 * @return 修改结果 */ Result<User> update(User user) throws Exception; /** * 判断用户是否登录(实际上就是从session取出用户id去数据库查询并比对) * * @param session 传入请求session * @return 返回结果,若用户已登录则返回用户信息 */ Result<User> isLogin(HttpSession session); }然后再在包service下建立包impl,然后在里面新建用户服务实现类UserServiceImpl:package com.example.userlogin.service.impl; import com.example.userlogin.api.UserAPI; import com.example.userlogin.dao.UserDAO; import com.example.userlogin.dataobject.User; import com.example.userlogin.model.Result; import com.example.userlogin.service.UserService; import com.example.userlogin.util.ClassExamine; import jakarta.servlet.http.HttpSession; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class UserServiceImpl implements UserService { @Autowired private UserDAO userDAO; @Override public Result<User> register(User user) { Result<User> result = new Result<>(); // 先去数据库找用户名是否存在 User getUser = userDAO.getByUsername(user.getUsername()); if (getUser != null) { result.setResultFailed("该用户名已存在!"); return result; } // 加密储存用户的密码 user.setPassword(DigestUtils.md5Hex(user.getPassword())); // 存入数据库 userDAO.add(user); // 返回成功消息 result.setResultSuccess("注册用户成功!", user); return result; } @Override public Result<User> login(User user) { Result<User> result = new Result<>(); // 去数据库查找用户 User getUser = userDAO.getByUsername(user.getUsername()); if (getUser == null) { result.setResultFailed("用户不存在!"); return result; } // 比对密码(数据库取出用户的密码是加密的,因此要把前端传来的用户密码加密再比对) if (!getUser.getPassword().equals(DigestUtils.md5Hex(user.getPassword()))) { result.setResultFailed("用户名或者密码错误!"); return result; } // 设定登录成功消息 result.setResultSuccess("登录成功!", getUser); return result; } @Override public Result<User> update(User user) throws Exception { Result<User> result = new Result<>(); // 去数据库查找用户 User getUser = userDAO.getById(user.getId()); if (getUser == null) { result.setResultFailed("用户不存在!"); return result; } // 检测传来的对象里面字段值是否为空,若是就用数据库里面的对象相应字段值补上 if (!StringUtils.isEmpty(user.getPassword())) { // 加密储存 user.setPassword(DigestUtils.md5Hex(user.getPassword())); } // 对象互补 ClassExamine.objectOverlap(user, getUser); // 存入数据库 userDAO.update(user); result.setResultSuccess("修改用户成功!", user); return result; } @Override public Result<User> isLogin(HttpSession session) { Result<User> result = new Result<>(); // 从session中取出用户信息 User sessionUser = (User) session.getAttribute(UserAPI.SESSION_NAME); // 若session中没有用户信息这说明用户未登录 if (sessionUser == null) { result.setResultFailed("用户未登录!"); return result; } // 登录了则去数据库取出信息进行比对 User getUser = userDAO.getById(sessionUser.getId()); // 如果session用户找不到对应的数据库中的用户或者找出的用户密码和session中用户不一致则说明session中用户信息无效 if (getUser == null || !getUser.getPassword().equals(sessionUser.getPassword())) { result.setResultFailed("用户信息无效!"); return result; } result.setResultSuccess("用户已登录!", getUser); return result; } }需要注意的是服务接口需要有@Service注解,接口实现类要有@Component注解,还自动注入了DAO的实例进行数据库操作。这里着重强调一下update操作。可以看见,在DAO层的update的SQL语句中并没有判断传入的用户对象的字段是否为空或者是否和原来一致,而是直接将传入的用户对象覆盖至数据库中相应的用户记录上了。因此,一般情况下,我们在Service层进行判断。一般来说,Service层update方法会先从数据库取出原始用户信息(上述名为getUser的对象),然后和传入的用户信息对象(上述名为user的对象)进行字段对比。如果传入的user中某个字段为空,说明这个字段的信息是不需要修改的,这时使用原始用户对象的相应字段值填上去,如果不为空,则保留其值,最后再传入DAO层修改数据库。也因此,前端在发起修改请求时,也会将不用修改的字段留空。这就是我们平时进行对象更新的逻辑。不过当一个用户的字段多了,我们是不是要写很多个if来逐一判断呢?当然是不行的。所以这里我封装了一个方法,利用反射,检测被补全的对象(传入对象)和完整对象(从数据库取出的对象)的字段,如果被补全对象某个字段为空,这说明这个字段值是不用修改的,用完整对象对应的字段值补全,不为空说明被补全对象(传入对象)中这个字段值是新的值,是要修改的,因此保持其不变。当然,密码是特殊的字段,因为需要加密储存,因此需要单独判断前端是否传入了新的密码,如果是就加密并赋给相应字段。在上述代码中我也进行了判断。这里新建util包,新建ClassExamine类封装一个补全对象的方法,如下:package com.example.userlogin.util; import org.apache.commons.lang3.StringUtils; import java.lang.reflect.Field; /** * 类检测实用类 */ public class ClassExamine { /** * 对象字段互补。传入一个同类型被补充对象和完整对象,如果被补充对象中有字段为null或者字符串为空,就用完整对象对应的字段值补上去;如果被补充对象中某字段不为空则保留它自己的值。 * * @param origin 被补充对象 * @param intactObject 完整对象 * @param <T> 传入对象类型 */ public static <T> void objectOverlap(T origin, T intactObject) throws Exception { Field[] fields = origin.getClass().getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); if (field.getType() == String.class) { if (StringUtils.isEmpty((String) field.get(origin))) { field.set(origin, field.get(intactObject)); } } else { if (field.get(origin) == null) { field.set(origin, field.get(intactObject)); } } } } }以及上述判断用户登录的方法,其中的session是从Controller类中传来的,(因为Controller类可以获取请求中的session),这里先不要纠结session的问题,我们往下看。7,创建用户登录API服务层写完了,现在就是前后端交互的桥梁需要打通了-编写API,这样前端才能发送请求调用我们后端的服务。我们的API要实现用户登录、注册、判断用户是否登录、修改用户信息、用户登出这几个功能。新建包api,然后在里面新建类UserAPI:package com.example.userlogin.api; import com.example.userlogin.dataobject.User; import com.example.userlogin.model.Result; import com.example.userlogin.service.UserService; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; @RestController public class UserAPI { /** * session的字段名 */ public static final String SESSION_NAME = "userInfo"; @Autowired private UserService userService; /** * 用户注册 * * @param user 传入注册用户信息 * @param errors Validation的校验错误存放对象 * @param request 请求对象,用于操作session * @return 注册结果 */ @PostMapping("/register") public Result<User> register(@RequestBody @Valid User user, BindingResult errors, HttpServletRequest request) { Result<User> result; // 如果校验有错,返回注册失败以及错误信息 if (errors.hasErrors()) { result = new Result<>(); result.setResultFailed(errors.getFieldError().getDefaultMessage()); return result; } // 调用注册服务 result = userService.register(user); return result; } /** * 用户登录 * * @param user 传入登录用户信息 * @param errors Validation的校验错误存放对象 * @param request 请求对象,用于操作session * @return 登录结果 */ @PostMapping("/login") public Result<User> login(@RequestBody @Valid User user, BindingResult errors, HttpServletRequest request) { Result<User> result; // 如果校验有错,返回登录失败以及错误信息 if (errors.hasErrors()) { result = new Result<>(); result.setResultFailed(errors.getFieldError().getDefaultMessage()); return result; } // 调用登录服务 result = userService.login(user); // 如果登录成功,则设定session if (result.isSuccess()) { request.getSession().setAttribute(SESSION_NAME, result.getData()); } return result; } /** * 判断用户是否登录 * * @param request 请求对象,从中获取session里面的用户信息以判断用户是否登录 * @return 结果对象,已经登录则结果为成功,且数据体为用户信息;否则结果为失败,数据体为空 */ @GetMapping("/is-login") public Result<User> isLogin(HttpServletRequest request) { // 传入session到用户服务层 return userService.isLogin(request.getSession()); } /** * 用户信息修改 * * @param user 修改后用户信息对象 * @param request 请求对象,用于操作session * @return 修改结果 */ @PutMapping("/update") public Result<User> update(@RequestBody User user, HttpServletRequest request) throws Exception { Result<User> result = new Result<>(); HttpSession session = request.getSession(); // 检查session中的用户(即当前登录用户)是否和当前被修改用户一致 User sessionUser = (User) session.getAttribute(SESSION_NAME); if (sessionUser.getId() != user.getId().intValue()) { result.setResultFailed("当前登录用户和被修改用户不一致,终止!"); return result; } result = userService.update(user); // 修改成功则刷新session信息 if (result.isSuccess()) { session.setAttribute(SESSION_NAME, result.getData()); } return result; } /** * 用户登出 * * @param request 请求,用于操作session * @return 结果对象 */ @GetMapping("/logout") public Result<Void> logout(HttpServletRequest request) { Result<Void> result = new Result<>(); // 用户登出很简单,就是把session里面的用户信息设为null即可 request.getSession().setAttribute(SESSION_NAME, null); result.setResultSuccess("用户退出登录成功!"); return result; } }因为是API,所以使用@RestController标注类,可见注册是通过前端发送POST请求后端接收,修改用的则是PUT请求,且在各个方法中加入参数HttpServletRequest request,这个名为request的对象就代表客户端这次对这个接口的访问的请求。可以通过这个名为request对象对session进行获取。每一个不同的请求都会有一个唯一的session,每个session中的信息都是key-value形式储存,这里我们只在session里面储存用户信息,因此我们把用户信息的key设为一个固定的名字userInfo,通过建立个常量SESSION_NAME。每个不同机器的请求都会生成独一无二的session,上面这段代码操作,我们可以理解为:登录/注册用户后,取得了用户信息,我们将每个不同机器的用户信息储存在了与它们相对应的session里面,并在其中设定用户信息的key为userInfo。其实,session中内容储存形式和cookie是一样的,都是key-value的形式,我们可以理解为和Java中的Map对象是差不多的。这里session指的是session机制中储存在服务端的用户信息。通过HttpServletRequest的getSession方法,即可获取这个请求对应的session对象,为HttpSession类型,其中setAttribute方法用于设定session中的键值对,getAttribute方法用于获取session中信息。可见,session中的内容是由我们自定义的,只不过通常我们需要把用户对象存进去以验证是否登录。上述我们只存放了用户对象在session里面,实际开发中大家还可以存点别的东西进去。当然不建议存太多信息,否则会增加服务器负载。这里我们还将session对象传入上述Service层的判断用户是否登录方法中,在其中对session中用户信息进行读取,然后利用session中用户对象的id去数据库中取出并进行比对判断用户是否有效登录。用户没有登录,则session获取到的用户对象就一定是null。对于这里API类的代码,建议大家可以联系上面的session机制示意图一起看,这样就更好理解。8,配置cookie属性其实到上面第7步,我们的功能基本上完整了,但是还有一些重要配置需要进行。我们还要开启session功能,并配置cookie属性例如过期时间等等。新建包config,在其中建立配置类SessionConfig:package com.example.userlogin.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.session.MapSessionRepository; import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; import org.springframework.session.web.http.CookieSerializer; import org.springframework.session.web.http.DefaultCookieSerializer; import java.util.concurrent.ConcurrentHashMap; /** * session配置类 */ @Configuration @EnableSpringHttpSession public class SessionConfig { /** * 设定cookie序列化器的属性 */ @Bean public CookieSerializer cookieSerializer() { DefaultCookieSerializer serializer = new DefaultCookieSerializer(); serializer.setCookieName("JSESSIONID"); // 用正则表达式配置匹配的域名,可以兼容 localhost、127.0.0.1 等各种场景 serializer.setDomainNamePattern("^.+?\.(\w+\.[a-z]+)$"); // cookie生效路径 serializer.setCookiePath("/"); // 设置是否只能服务器修改,浏览器端不能修改 serializer.setUseHttpOnlyCookie(false); // 最大生命周期的单位是分钟 serializer.setCookieMaxAge(24 * 60 * 60); return serializer; } /** * 注册序列化器 */ @Bean public MapSessionRepository sessionRepository() { return new MapSessionRepository(new ConcurrentHashMap<>()); } }注意配置类打上@Configuration注解,@EnableSpringHttpSession开启session。9,配置拦截器虽然我们有判断用户登录的API,但是如果我们页面很多,每一个都要判断登录,就会很麻烦。通过拦截器即可对指定的路径设定拦截点。继续在config包下创建拦截器类UserInterceptor:package com.example.userlogin.config; import com.example.userlogin.service.UserService; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; /** * 拦截器 */ public class UserInterceptor implements HandlerInterceptor { @Autowired private UserService userService; // Controller方法执行之前 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 同样在这里调用用户服务传入session,判断用户是否登录或者有效 // 未登录则重定向至主页(假设主页就是/) if (!userService.isLogin(request.getSession()).isSuccess()) { response.sendRedirect("/"); return false; } return true; } // Controller方法执行之后 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } // 整个请求完成后(包括Thymeleaf渲染完毕) @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }可见拦截器有三个方法,分别对应三个切入点。一般我们只需要修改在Controller执行之前的那个,它可以像API一样操作session,返回true时表示允许继续访问这个Controller,否则终止访问。通过HttpServletResponse的sendRedirect方法可以发送重定向。拦截器类写好了,接下来就是注册拦截器了。在config类中创建配置类InterceptorRegister:package com.example.userlogin.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.ArrayList; import java.util.List; /** * 拦截器注册 */ @Configuration public class InterceptorRegister implements WebMvcConfigurer { /** * 把我们定义的拦截器类注册为Bean */ @Bean public HandlerInterceptor getInterceptor() { return new UserInterceptor(); } /** * 添加拦截器,并配置拦截地址 */ @Override public void addInterceptors(InterceptorRegistry registry) { List<String> pathPatterns = new ArrayList<>(); pathPatterns.add("/update"); registry.addInterceptor(getInterceptor()).addPathPatterns(pathPatterns); } }在addInterceptors方法里面,我们进行拦截器注册,并配置拦截地址。上述例子只添加拦截/update这个路径,其余不拦截。还可以设定拦截全部,只排除/login,如下写addInterceptors方法:@Override public void addInterceptors(InterceptorRegistry registry) { List<String> pathPatterns = new ArrayList<>(); pathPatterns.add("/login"); registry.addInterceptor(getInterceptor()).excludePathPatterns(pathPatterns); }在此,一个比较简易但完整的用户注册登录就做完了!10,体验一下启动程序,并使用ApiPost软件来体验一下用户登录等服务接口的功能。(1) 注册一个用户先访问/register注册一个用户:(2) 测试判断登录接口先不急着登录,访问/is-login看看:可见由于我们还未登录,所以请求中没有sessionId,后端也无法找到这个请求的session数据,因此返回未登录。(3) 登录然后再次判断这次我们访问/login登录:然后再访问/is-login接口:可见这一次,我们的请求中就带着sessionId了!服务端也可以找到对应的session。现在许多系统都是前后端分离的系统,因此这个判断登录/is-login接口也是很有必要的,除了判断这个请求是否已经登录之外,还可以直接拿取已登录的用户的信息,而无需传入id去查询。11,总结用户登录注册看起来要写的东西很多,但实际上流程很清晰,也不难理解。我们发现,DAO层就是单纯操作数据库,返回用户对象;Service层基本上就是用于验证信息正确性,返回封装的Result对象;Controller层进一步设定session,也是返回封装Result对象。每个不同的部分各司其职,完成了我们整个业务逻辑。其实不仅仅是用户登录系统,我们使用Spring Boot搭建任何系统,无外乎都是以下几个大步骤:构建数据模型:dataobject包的内容和model包的内容,dataobject一般是数据库中的对象,最好是画个类图编写DAO层:用于操作数据库,定义好需要用到的操作数据库的方法例如增删改查、根据用户名查找用户等等编写Service层:即为服务层,用于调用DAO层,一般来说我们会把大量的代码写在这里,这里包含了许多逻辑,一个网站有哪些服务(用户登录,注册等等),都定义在这层,这一层是操作DAO层并处理数据的一层,也包含了业务逻辑编写API层:即为接口,是处在最外面的一层了,调用Service层,是前后端交流的桥梁示例程序仓库地址:传送门ApiPost测试配置:传送门
在Spring Boot集成Kafka时,大家都知道可以使用@KafkaListener注解创建消费者。但是@KafkaListener注解是静态的,意味着在编译时就已经确定了消费者,无法动态地创建消费者。不过事实上,使用Kafka提供的Java API,使用KafkaConsumer类就可以完成消费者的动态创建。我们也知道在一个消费者组中,同一条消息只会被消费一次。而动态创建消费者的情景也通常是满足动态的发布订阅模型(一个发布者,但是可能有不定量的消费者),所以在这里我们使每个动态创建的消费者的消费者组也不一样即可。下面,我们就来实现一下这个功能。1,创建消费者对象我们可以定义一个“消费者工厂”类,专门用于创建Kafka消费者对象,如下:package com.gitee.swsk33.kafkadynamicconsumer.factory; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.kafka.KafkaProperties; import org.springframework.stereotype.Component; import java.util.Collections; import java.util.Properties; @Component public class KafkaDynamicConsumerFactory { @Autowired private KafkaProperties kafkaProperties; @Value("${spring.kafka.consumer.key-deserializer}") private String keyDeSerializerClassName; @Value("${spring.kafka.consumer.value-deserializer}") private String valueDeSerializerClassName; /** * 创建一个Kafka消费者 * * @param topic 消费者订阅的话题 * @param groupId 消费者组名 * @return 消费者对象 */ public <K, V> KafkaConsumer<K, V> createConsumer(String topic, String groupId) throws ClassNotFoundException { Properties consumerProperties = new Properties(); // 设定一些关于新的消费者的配置信息 consumerProperties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaProperties.getBootstrapServers()); // 设定新的消费者的组名 consumerProperties.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); // 设定反序列化方式 consumerProperties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, Class.forName(keyDeSerializerClassName)); consumerProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, Class.forName(valueDeSerializerClassName)); // 设定信任所有类型以反序列化 consumerProperties.put("spring.json.trusted.packages", "*"); // 新建一个消费者 KafkaConsumer<K, V> consumer = new KafkaConsumer<>(consumerProperties); // 使这个消费者订阅对应话题 consumer.subscribe(Collections.singleton(topic)); return consumer; } }可见这里我们注入了配置文件中反序列化的配置,并用于新创建的消费者对象。2,使用定时任务实现消费者实时订阅上面仅仅是创建了消费者,但是消费者接收消息以及处理消息的操作,也是需要我们手动定义的。如何让创建的消费者都去不停的接收并处理我们的消息呢?大致思路如下:使用定时任务,在定时任务中使消费者不停地接收并处理消息与此同时,将每个定时任务和消费者都存起来,后面在消费者不需要的时候可以移除它们并关闭定时任务这里,我们编写一个上下文类,用于存放所有的消费者和定时任务,并编写增加和移除定时任务的方法:package com.gitee.swsk33.kafkadynamicconsumer.context; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.KafkaConsumer; import java.time.Duration; import java.util.Map; import java.util.concurrent.*; /** * Kafka消费者任务上下文 */ public class KafkaConsumerContext { /** * 存放所有自己创建的Kafka消费者任务 * key: groupId * value: kafka消费者任务 */ private static final Map<String, KafkaConsumer<?, ?>> consumerMap = new ConcurrentHashMap<>(); /** * 存放所有定时任务的哈希表 * key: groupId * value: 定时任务对象,用于定时执行kafka消费者的消息消费任务 */ private static final Map<String, ScheduledFuture<?>> scheduleMap = new ConcurrentHashMap<>(); /** * 任务调度器,用于定时任务 */ private static final ScheduledExecutorService executor = Executors.newScheduledThreadPool(24); /** * 添加一个Kafka消费者任务 * * @param groupId 消费者的组名 * @param consumer 消费者对象 * @param <K> 消息键类型 * @param <V> 消息值类型 */ public static <K, V> void addConsumerTask(String groupId, KafkaConsumer<K, V> consumer) { // 先存入消费者以便于后续管理 consumerMap.put(groupId, consumer); // 创建定时任务,每隔1s拉取消息并处理 ScheduledFuture<?> future = executor.scheduleAtFixedRate(() -> { // 每次执行拉取消息之前,先检查订阅者是否已被取消(如果订阅者不存在于订阅者列表中说明被取消了) // 因为Kafka消费者对象是非线程安全的,因此在这里把取消订阅的逻辑和拉取并处理消息的逻辑写在一起并放入定时器中,判断列表中是否存在消费者对象来确定是否取消任务 if (!consumerMap.containsKey(groupId)) { // 取消订阅并关闭消费者 consumer.unsubscribe(); consumer.close(); // 关闭定时任务 scheduleMap.remove(groupId).cancel(true); return; } // 拉取消息 ConsumerRecords<K, V> records = consumer.poll(Duration.ofMillis(100)); for (ConsumerRecord<K, V> record : records) { // 自定义处理每次拉取的消息逻辑 System.out.println(record.value()); } }, 0, 1, TimeUnit.SECONDS); // 将任务存入对应的列表以后续管理 scheduleMap.put(groupId, future); } /** * 移除Kafka消费者定时任务并关闭消费者订阅 * * @param groupId 消费者的组名 */ public static void removeConsumerTask(String groupId) { if (!consumerMap.containsKey(groupId)) { return; } // 从列表中移除消费者 consumerMap.remove(groupId); } }在增加消费者定时任务的方法中,调用消费者对象的poll方法能够拉取一次消息,一次通常可能拉取到多条消息,遍历并处理即可。这样在定时任务中,我们每隔一段时间就拉取一次消息并处理,就实现了消费者实时订阅消息的效果。除此之外,在使用定时任务时,即ScheduledExecutorService对象的scheduleAtFixedRate方法,可以实现每隔一定的时间执行一次任务,上述第一个参数传入Runnable接口的实现类,这里使用匿名内部类传入,即自定义的任务,第二个参数是启动延迟时间,第三个参数是每隔多长时间重复执行任务,第四个参数是时间单位。该方法返回一个任务对象,通过这个对象的cancel方法可以取消掉任务。可见这里,在定时任务中,每次拉取消息之前先判断消费者是否还存在于列表中,以确定消费者是否被取消。为什么要这么操作呢?因为Kafka的消费者对象是非线程安全的,而ScheduledExecutorService底层使用的是线程池来完成定时任务,如果说我们把取消消费者订阅的逻辑写在另一个方法中,就会导致有两个线程同时操作Kafka消费者,从而抛出异常(定时器线程一直在操作消费者拉取消息,取消订阅又是从定时器之外的线程操作的,这就有两个线程),使得我们不能正常地关闭消费者。(异常内容:kafkaconsumer is not safe for multi-threaded access)所以这里,我把拉取消息逻辑和取消订阅逻辑都写在了一起放在一个定时任务中,使得拉取消息和取消订阅者的操作都是在同一线程(即定时器中线程)执行,而判断是否要取消订阅者的依据就是检查该订阅者是否从订阅者列表中被移除。3,编写个API测试现在编写一个API测试一下效果:package com.gitee.swsk33.kafkadynamicconsumer.api; import com.gitee.swsk33.kafkadynamicconsumer.context.KafkaConsumerContext; import com.gitee.swsk33.kafkadynamicconsumer.factory.KafkaDynamicConsumerFactory; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.kafka.core.KafkaTemplate; 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; /** * 消息测试api */ @RestController @RequestMapping("/api/kafka") public class KafkaTestAPI { @Autowired private KafkaTemplate<String, String> kafkaTemplate; @Autowired private KafkaDynamicConsumerFactory factory; @GetMapping("/send") public String send() { kafkaTemplate.send("my-topic", "hello!"); return "发送完成!"; } @GetMapping("/create/{groupId}") public String create(@PathVariable String groupId) throws ClassNotFoundException { // 这里统一使用一个topic KafkaConsumer<String, String> consumer = factory.createConsumer("my-topic", groupId); KafkaConsumerContext.addConsumerTask(groupId, consumer); return "创建成功!"; } @GetMapping("/remove/{groupId}") public String remove(@PathVariable String groupId) { KafkaConsumerContext.removeConsumerTask(groupId); return "移除成功!"; } }现在依次访问/api/kafka/create/a和/api/kafka/create/b,就创建了两个消费者,然后访问/api/kafka/send发送消息,结果如下:可见,两个消费者都接收到了消息。4,总结可见要动态地创建Kafka消费者,只需创建并设置好Kafka消费者对象,并使用定时任务使它们一直拉取消息,就可以实现发布订阅的效果。当然,我们要管理好创建的所有的消费者和定时任务,防止资源浪费。上述示例仓库地址:传送门
在Java中,我们经常会提到面向接口编程,这样减少了模块之间的耦合,更加灵活。在一个项目中我们也通常将接口和实现类放在一起,但是如果哪天我们要替换其它的实现类,或者是修改实现类,涉及到实现类的代码也要相应地修改。能不能这样:在调用服务的时候,我们只调用接口,不用关心实现类呢?无论我们怎么切换实现类,调用接口的部分代码都能正常运行?当然是可以的,Java SPI (Service Provider Interface)就提供了这样的机制。Java SPI机制中,我们不再是手动指定接口和实现类的关系,而是让接口去寻找可用的实现类。事实上,我们经常使用的Spring框架、日志接口等等,都是使用了SPI机制实现了扩展。1,SPI和API在说起SPI之前,我们还是先看一下API,API我们已经很熟悉了,和SPI都可以被称作接口。只不过API的功能的实现,以及接口的定义全部是接口的实现者提供的,调用者只需要调用接口即可:不过SPI就不一样了,在SPI机制中,调用者仍然是调用接口,但是这个接口是独立存在的,并且可以由不同的实现者实现:也就是说,这里接口只是一个标准,并且提供接口的那一方并不一定回去实现接口,而是根据接口的定义,由更多的第三方实现。这个接口可以由一个甚至是多个实现者去实现。也因此,调用者在调用接口时,可能还需要指定一下使用哪个实现者的实现类。实现者也叫做服务提供者。事实上,我们日常生活中经常使用的U盘也很类似SPI机制,U盘使用的是USB接口,USB接口仅仅是一个规范(接口),但是发明USB接口的公司并没有去生产U盘,而是由不同的U盘厂商例如金士顿、闪迪(实现者)等等去根据这个规范生产U盘,然后我们就可以去选择自己喜欢的牌子(选择实现者)购买U盘,不过平时无论使用什么牌子的U盘,我们只需要插入到电脑的USB接口(调用接口)即可使用,而不用关心不同的厂商是怎么实现USB接口的功能的。可见,SPI机制将实现者和接口再次解耦合了,使得接口更加易于扩展。事实上,我们常常用的SLF4J就是一个Java的日志接口,但是它也仅仅是一个接口,所以被称作门面。而它的实现有Logback、Log4j等等,并且在切换实现的时候,我们只需要修改一下依赖配置即可,代码并不需要任何变动,因为代码中也仅仅是调用了接口。2,自己完成一个SPI那么现在,我们也来以一个最简单的日志接口为例,实现自己的SPI。(1) 定义SPI接口先新建一个空的Maven项目log-interface,然后在里面创建一个日志接口,声明日志接口具备的方法(功能):package com.gitee.swsk33.loginterface.spi; /** * 定义日志接口 */ public interface Logger { /** * INFO级别日志方法 * * @param message 日志打印消息 */ void info(String message); /** * DEBUG级别日志方法 * * @param message 日志打印消息 */ void debug(String message); }这样,我们便定义了这么一个日志接口,并声明日志接口需要有info和debug这两个日志功能。然后就是编写服务类,这个服务类是这里最为重要的地方,它的作用是扫描所有实现了Logger接口的实现类并加载进来,然后供调用者去调用。先看代码:package com.gitee.swsk33.loginterface.service; import com.gitee.swsk33.loginterface.spi.Logger; import java.util.ArrayList; import java.util.List; import java.util.ServiceLoader; /** * 服务,用于加载所有服务使用者的实现类,以及供外部调用 * 该类为一个单例 */ public class LoggerService { /** * 该类唯一单例 */ private static final LoggerService LOGGER = new LoggerService(); /** * 默认的Logger实现类 */ private final Logger defaultLogger; /** * 所有的Logger实现类列表 */ private final List<Logger> allLoggers = new ArrayList<>(); /** * 私有化构造器 */ private LoggerService() { // 加载全部Logger接口的实现类 ServiceLoader<Logger> loader = ServiceLoader.load(Logger.class); // 将实现类放入我们的Logger实现类列表 for (Logger logger : loader) { allLoggers.add(logger); } // 这里取出第一个作为默认实现类 if (!allLoggers.isEmpty()) { defaultLogger = allLoggers.get(0); } else { defaultLogger = null; } System.out.println("加载到" + allLoggers.size() + "个服务实现!"); } /** * 获取该服务类的唯一单例 * * @return 该服务类的唯一单例 */ public static LoggerService getInstance() { return LOGGER; } /** * 调用默认的实现类的info日志打印方法 * * @param message 消息 */ public void info(String message) { if (defaultLogger == null) { System.err.println("没有找到实现了Logger接口的类!"); return; } defaultLogger.info(message); } /** * 调用默认的实现类的debug日志打印方法 * * @param message 消息 */ public void debug(String message) { if (defaultLogger == null) { System.err.println("没有找到实现了Logger接口的类!"); return; } defaultLogger.debug(message); } }首先这个类是一个单例的类,在构造器中,我们使用ServiceLoader这个类来将实现了Logger接口的所有类都扫描进来,并存入我们的实现类列表,然后我们取出列表中的第一个作为默认实现。在下面我们定义了info和debug来完成对接口的默认实现类的调用。最后,在项目目录下执行mvn install命令将其安装至本地Maven仓库,以便后续服务提供者引入并实现。(2) 完成一个接口的实现现在再新建一个空的Maven项目logservice-one,并引入上面接口项目为依赖:然后编写实现类:package com.gitee.swsk33.logserviceone.service; import com.gitee.swsk33.loginterface.spi.Logger; /** * Logger SPI的实现类 */ public class LogOne implements Logger { @Override public void info(String s) { System.out.println("[LogOne INFO] " + s); } @Override public void debug(String s) { System.out.println("[LogOne DEBUG] " + s); } }然后在resources目录下创建目录META-INF/services,这个目录中是用于声明该服务实现中有哪些实现类实现了什么接口。在这个目录下我们新建一个文件名为com.gitee.swsk33.loginterface.spi.Logger,文件中的内容为:com.gitee.swsk33.logserviceone.service.LogOne可见,该目录下文件名是要实现的接口的全限定类名(包名 + 类名),而文件中内容是实现了该接口的实现类的全限定类名。大家参考这里的文件名及其中的内容,与我们上述的接口全限定类名、实现类全限定类名对比一下就知道了!如果说这个项目中有多个类实现了Logger接口,那么我们都需要在文件中声明,一行一个实现类的全限定类名。最终整个项目结构如下:同样地,最后记得在项目目录下执行mvn install命令将其安装至本地Maven仓库,以便调用者调用。(3) 测试接口这里再新建一个Maven空项目log-test,作为接口的调用者,在依赖中引入实现者:然后创建一个主类调用一下接口试试:package com.gitee.swsk33.logtest; import com.gitee.swsk33.loginterface.service.LoggerService; public class Main { private static final LoggerService LOGGER = LoggerService.getInstance(); public static void main(String[] args) { LOGGER.info("测试info消息"); LOGGER.debug("测试debug消息"); } }结果:可见,我们成功地调用了Logger接口中的方法。通常调用者的依赖中可能会同时引入SPI接口依赖和服务提供者(实现)的依赖,这样也没问题,不过通常服务提供者本身就依赖于SPI接口,因此只引入服务提供者依赖,也会间接地引入SPI接口依赖,不影响我们调用SPI接口。我们这里只有一个服务提供者logservice-one,如果说还有logservice-two等等多个服务提供者,我们只需要在依赖中更换一下即可,代码完全不需要改变。也可见调用者在调用接口的时候,只需要关注接口就行了,不需要关心实现类。3,再看ServiceLoader可见在SPI接口中,我们使用ServiceLoader完成了对所有实现了Logger接口的类的扫描和加载,那么具体的过程是什么样的呢?如果大家去查看这个类的源码,可以发现它实现了Iterable接口,这也说明我们可以通过迭代的方式去完成多个实现类的切换。然后在其源码中,有这么一个常量定义:static final String PREFIX = "META-INF/services/";这就说明,ServiceLoader会去扫描服务提供者的classpath路径下的META-INF/services目录,来扫描哪些类实现了指定接口,而其静态方法load的参数,正是指定了被实现的接口。也因此我们要在服务提供者的项目的resources目录下创建这个目录并申明接口和对应实现类的全限定类名。在Maven项目中,resources目录就对应的是classpath的根目录。简而言之,ServiceLoader加载实现类的过程如下:先是调用load方法并指定要扫描的接口然后扫描项目中META-INF/services目录,这包括调用者项目以及它所引入的所有依赖包中的META-INF/services目录下的声明扫描到所有实现类后,根据其类名,先判断是否跟SPI接口为同一类型,如果是则利用反射的方式将所有实现类实例化,加载进内存,并返回所有实现类的实例列表可见,这就是JDK中SPI机制加载服务的大致过程,事实上,现在很多框架也利用SPI机制实现了灵活地扩展。示例仓库地址:传送门
Alpine是一个极其轻量级的Linux,通常用作制作Docker镜像,今天就来分享一下如何在Alpine容器中安装配置ssh并远程连接。1,安装ssh服务端创建容器后,就可以通过命令进行安装配置了,记得先把容器的22端口映射出来!首先是修改镜像源为国内镜像源(清华大学镜像站),不然下载安装很慢,进入容器后执行:sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories然后安装ssh服务端:apk add --no-cache openssh-server安装完成后,进入/etc/ssh目录生成密钥:cd /etc/ssh ssh-keygen -A到此,就安装完成了!2,启动sshd服务通过下列命令启动:/usr/sbin/sshd没有任何输出说明启动成功,这个时候就可以在容器外通过ssh访问了!需要注意的是,由于容器中使用open-rc服务管理器有很多限制,因此这里是使用的手动启动的方式,这意味着每次重启容器sshd不会启动,因此进入容器需要手动执行上述命令启动服务。如果是要自己制作镜像并集成ssh,那么可以将上述启动命令写在Dockerfile中的CMD字段作为容器启动命令。3,配置文件如需修改配置文件,参考这篇博客即可。修改完成配置文件后,通过下列命令重启sshd即可:kill -9 $(pidof sshd) /usr/sbin/sshd
Docker为我们在服务器上部署应用程序提供了很大的便利,并使得我们能够更好地管理我们部署的应用程序以及一些中间件等等。不过我们通常在Windows上面进行开发的时候,也需要在本地安装数据库、部分中间件等等,安装多了也很麻烦,或者是开虚拟机模拟一个服务器,这导致了很大的性能开销。因此,在Windows电脑上面安装Docker,并在本地也将开发调试用的数据库、中间件等容器化管理,是一个非常好的选择。1,安装WSL2内核通常Docker Desktop可以使用WSL2或者Hyper-V作为其核心以运行容器,不过官方更加推荐使用WSL2,因为性能更好,因此在安装Docker Desktop之前,最好是先安装一下WSL2内核。如果之前安装过WSL并使用过子系统,这一步可以跳过。在安装WSL2内核之前,记得先去主板BIOS设置中开启CPU虚拟化功能(Intel主板是VT,AMD主板是SVM),否则会导致安装失败或者内核启动失败,这里就不再赘述如何开启了,非常简单的,大家自行查阅即可。然后在Windows中打开cmd或者其它终端,输入WSL2的安装命令:wsl --install --no-distribution可见我们安装时加上了--no-distribution参数,表示仅安装内核而不安装任何发行版文件,因为Docker Desktop只是依赖于WSL2内核,不需要任何发行版文件。等待安装完成后,会提示你重启:重启电脑,WSL2内核就安装完成了!如果说执行上述安装命令时,报错没有--no-distribution选项并输出了很多帮助提示信息,说明你已经安装过WSL2内核了,不需要再重复安装,直接进行下一步安装Docker Desktop即可。2,安装Docker Desktop进入官方下载页面,下载Windows版的:然后安装,安装时将这两个选项都勾选上:等待安装完成:第一次安装会提示要重启,点击安装完成界面的Close and restart按钮重启电脑。重启完成电脑后,Docker Desktop就会自动开启,第一次使用会弹出同意协议的窗口,点击Accept按钮即可,然后进入主界面会显示正在启动Docker Engine,初次启动会要一点时间,耐心等待即可。3,一些设置打开Docker Desktop后,点击右上角齿轮图标即可进入设置。(1) 开机自启等常用配置在General选项卡中,勾选下图第一个选项Start Docker Desktop when you log in,即可开机自启动,推荐打开开机自启动,这样一开机就可以访问我们一些容器的服务。在这个页面,还有下列配置建议设定:Send usage statistics 发送错误报告,可以取消勾选Show weekly tips 显示每周小提示,建议取消勾选Open Desktop Dashboard at startup 每次启动Docker就弹出主面板,建议取消勾选,需要用的时候再在任务栏打开更好(2) 国内镜像源配置拉取镜像通常速度很慢,因为镜像仓库位于国外,这时我们配置一下阿里云加速即可。首先进入这个链接:https://cr.console.aliyun.com/ ,登录自己的阿里云账户,在左侧容器工具-镜像加速器中,复制自己的加速器地址:然后进入Docker Engine选项卡,可见右侧JSON内容就是配置文件:在里面添加一个镜像源字段:"registry-mirrors": ["你的容器加速地址"]最终如下:注意registry-mirrors的位置!最后,点击右下角Apply & restart按钮即可保存配置。4,使用到此,我们就可以正常使用Docker了!打开命令行或者cmd,即可在里面使用docker命令。如果仍然提示命令不存在,则将下列路径加入Path环境变量:C:\Program Files\Docker\Docker\resources\bin这里命令和在Linux上是一模一样的。这里大家就可以试试去创建一个MySQL镜像运行在本地电脑上并自己连接了!还有其它数据库、中间件等都可以在本地容器化管理,容器端口映射出来后访问127.0.0.1的对应端口,即可访问到对应容器。5,访问数据卷在Windows中挂载的数据卷,都是存放在WSL虚拟机中的。我们通过命令行看到的数据卷仍然是Linux路径格式:不过我们仍然可以访问,所有的数据卷都存放在\\wsl.localhost\docker-desktop-data\data\docker\volumes路径下,把这个路径粘贴到此电脑(资源管理器)上面地址栏中回车即可:可以看到我们的具名数据卷,进入可以修改或者增加删除文件等等。
WSL是适用于Linux的Windows子系统,相当于是在我们的Windows系统中安装一个小的Linux系统,WSL比起虚拟机或者双系统,无论是便利性还是性能上都有着不错的优势。1,安装WSL2首先我们要知道,一个Linux操作系统是由Linux内核和发行版文件组成。内核是整个操作系统的核心,而发行版文件提供了基本的系统命令和运行库等等。同样地,WSL2也由这两部分组成,在安装时,WSL2会将一个完整的Linux内核和发行版文件一起安装。也因此,WSL2可以安装多个发行版。在安装之前,记得先进入电脑主板BIOS中开启CPU虚拟化(VT)功能,否则会导致WSL2安装或者运行失败。在较新的Windows10或者Windows11系统中,都自带了wsl命令,通过该命令安装,打开cmd或者终端,执行下列命令安装:wsl --install这样,默认安装的是Ubuntu的发行版文件,如果想安装其它发行版,可以先通过下列命令查看有哪些可以用的发行版:wsl -l -o然后再在安装时加上-d参数指定要安装的发行版,例如我要安装Debian发行版:wsl --install -d Debian等待片刻其安装完成,可能会提醒你重启电脑,重启电脑后,会弹出WSL的命令行窗口要你设置Linux子系统中的用户名和密码:依次要输入的是用户名、密码和确认密码,然后设置完成,这个时候,子系统就安装完成了!2,进入子系统我们随时可以用下列命令进入子系统:wsl -d 发行版名称比如我安装的是Debian发行版,则:wsl -d Debian这样,就进入了子系统,你的终端也变成了子系统中的终端,如果安装了多个发行版,也可以同时开多个终端并通过上述命令指定发行版名并启动,输入exit即可退出子系统。这个Linux子系统和我们使用的真正的Linux系统几乎没有区别,安装完成后,大家可以像往常一样,设置软件镜像源,安装常用命令等等。3,访问子系统文件系统打开此电脑,就可以在左侧看到Linux这一栏,点击进入即可:不过在对其中文件进行操作时需要注意文件权限问题,详情查看官方文档。4,网络问题如果你在子系统中运行了例如Nginx的网络服务器,直接在Windows上通过localhost是可以直接访问的,访问对应端口即可。5,常用操作(1) 开启systemctl支持默认情况下,WSL2中的Linux子系统无法使用systemctl命令,这会使得一些应用程序无法正常启动。可以通过修改配置文件的方式来启用该命令。首先进入子系统,通过下列命令创建并编辑配置文件/etc/wsl.conf:sudo touch /etc/wsl.conf sudo vim /etc/wsl.conf若提示找不到vim则安装一下即可,记得先完成软件镜像源配置,和普通Linux系统中一模一样,或者换用vi命令也行。然后在配置文件中加入以下内容:[boot] systemd=true编辑完成后,用exit命令退出子系统,并重启内核:wsl --shutdown然后重新进入子系统即可。(2) 软件镜像源配置子系统的软件镜像源配置和真实的Linux系统配置是一模一样的,以Debian系Linux为例,参考这篇博客。(3) 子系统的命令自动补全在进入子系统时,大家可能会发现无法使用Tab命令补全功能,同样地,还是配置软件源后,通过下列命令安装自动补全功能:sudo apt install bash-completion(4) 子系统中文环境配置子系统默认是英文的环境,命令行输出的系统提示也都是英文的,因此我们也可以像普通Linux系统中一样设置中文语言环境,参考这篇博客。(5) 关闭WSL2内核即使是我们退出了子系统,WSL2的内核仍然是在后台运行的,这样会占用很多内存:在Windows中打开cmd或者终端,执行下列命令即可关闭内核:wsl --shutdown下次再进入子系统时,内核也会自动启动。(6) 卸载发行版如果要卸载已安装的发行版,执行:wsl --unregister 发行版名称这样,你的子系统及其所有文件都会被删除,不过内核不会被删除,下次可以重新安装发行版。参考:WSL2官方安装文档:传送门WSL2配置文件:传送门WSL2跨文件系统操作:传送门WSL2网络问题:传送门WSL2文件权限:传送门
在安装Linux之后,我们通常会先配置起国内软件源加速我们的软件包的安装。今天我将常用的Debian系Linux的国内软件源汇总一下。1,Debian系软件源格式说明通常Debian系的Linux配置软件源都是修改/etc/apt/sources.list文件,或者是在/etc/apt/sources.list.d中加入一些第三方的软件源文件等等。但是两者文件中格式都是统一的如下:deb 软件源地址 发行版代号 软件分支1 软件分支2 ... deb-src 软件源地址 发行版代号 软件分支1 软件分支2 ...配置项语法很简单,由deb开头的是表示二进制可执行软件的软件源,而deb-src开头的是软件源代码。文件中以#开头的是注释。通常配置了软件源之后,执行sudo apt update即可更新软件列表索引。例如Debian的软件源配置某一行如下:# Debian 11阿里镜像源某片段 deb https://mirrors.aliyun.com/debian bullseye main non-free contrib(1) 版本代号/水平划分部分在上述紧接着配置的地址后的bullseye就表示该系统版本代号,当前系统版本代号可以通过命令lsb_release -a查看,输出结果中的Codename就是当前系统版本代号。若提示找不到lsb_release命令,则通过以下命令安装lsb-release包:sudo apt install lsb-release知道了当前系统版本代号之后,我们可以先在浏览器打开镜像源地址,打开后通常是目录形式,在里面我们可以找到所有软件包:进入其中的dists文件夹,在里面可以看到所有的系统代号(这个目录中存放的是每个系统代号对应的软件列表索引):我们这里Debian 11的代号是bullseye,因此我们可以先在这找到bullseye代号及其相关部分:可见包含该系统代号的主要软件包及其水平划分一共有5个,这些是都要加进镜像源的,这五个包含的软件范畴不一样,有的是主要软件,有的是更新软件等等。这说明一个系统代号下的所有软件包会先被水平划分为几个分类。现在就可以先编辑软件源配置如下:# 阿里镜像源 deb https://mirrors.aliyun.com/debian bullseye deb https://mirrors.aliyun.com/debian bullseye-updates deb https://mirrors.aliyun.com/debian bullseye-proposed-updates deb https://mirrors.aliyun.com/debian bullseye-backports deb https://mirrors.aliyun.com/debian bullseye-backports-sloppy deb-src https://mirrors.aliyun.com/debian bullseye deb-src https://mirrors.aliyun.com/debian bullseye-updates deb-src https://mirrors.aliyun.com/debian bullseye-proposed-updates deb-src https://mirrors.aliyun.com/debian bullseye-backports deb-src https://mirrors.aliyun.com/debian bullseye-backports-sloppy好了我们把每个配置项的地址部分和代号部分写完了,但是还没有写软件分支部分,我们接下来看。(2) 软件分支/垂直划分部分配置中在紧接着系统代号后的几项就都是软件分支部分了!还是打开镜像源网址,进入pool文件夹(这个文件夹就存放的是所有软件包):可见pool中有三个目录,这三个目录就代表着Debian三个软件的垂直划分,可见Debian的软件包被划分为下列三种:main Debian中符合自由软件规范的软件包contrib 本身属于自由软件但是可能部分依赖非自由软件的软件包non-free 非自由软件好了,我们现在可以将上述的软件源配置的分支部分补齐了如下:# 阿里镜像源 deb https://mirrors.aliyun.com/debian bullseye main non-free contrib deb https://mirrors.aliyun.com/debian bullseye-updates main non-free contrib deb https://mirrors.aliyun.com/debian bullseye-proposed-updates main non-free contrib deb https://mirrors.aliyun.com/debian bullseye-backports main non-free contrib deb https://mirrors.aliyun.com/debian bullseye-backports-sloppy main non-free contrib deb-src https://mirrors.aliyun.com/debian bullseye main non-free contrib deb-src https://mirrors.aliyun.com/debian bullseye-updates main non-free contrib deb-src https://mirrors.aliyun.com/debian bullseye-proposed-updates main non-free contrib deb-src https://mirrors.aliyun.com/debian bullseye-backports main non-free contrib deb-src https://mirrors.aliyun.com/debian bullseye-backports-sloppy main non-free contribok,到这里就配置完成了!可见知道了软件源配置格式以及软件源地址,我们就可以自己配置软件源了!有少部分的发行版每个系统代号部分对应不同的软件包划分,因此除了查看pool目录中的软件包划分之外,还可以直接点进dist目录中的对应的系统代号部分查看:进入dist中对应代号的目录,查看其中的目录名即可(文件不管)。可见Debian发行版的系统代号下的软件包划分是和pool中的是对应的,如果说不对应则以dist中每个代号中的为准。Debian除了配置上述主要软件源以外,还有安全更新源,下面会贴出。其余系统的系统代号部分和软件包划分可能有所不同,例如Ubuntu源中,dists和pool中如下:可见Ubuntu和Debian不同,软件包划分为四个分支,意义也不一样,具体大家可以自行了解。对于Deepin就很简单了,其没有水平划分:可见即使是不同的系统,软件源配置方式及其划分方式都是几乎一样的,总结而来如下:水平划分:每个Linux系统的某个版本都有其对应的代号,而该代号版本的系统的所有软件包通常会先按照功能水平划分,例如Debian的bullseye代号下所有软件包先被水平划分为了5个类别:bullseye、bullseye-updates、bullseye-proposed-updates、bullseye-backports和bullseye-backports-sloppy,水平划分部分可以在软件源网址中的dists目录中看到,当然不是每个系统的软件包都会水平划分,例如Deepin 20的软件包就没有进行水平划分,那么就直接在dists中找到其版本代号即可垂直划分:每个水平划分下的软件包通常又会根据软件包自由程度进行垂直划分,而通常一个系统中的每个水平划分下的垂直划分都是一样的。例如Debian中每个水平划分下都垂直划分为了3类:main、contrib和non-free,在软件源网址中的pool目录中可以看到垂直划分2,常用Debian系Linux国内镜像源修改镜像源配置的方法都基本上一样,修改/etc/apt/sources.list文件,将里面全部内容先删掉或者注释掉,然后选择下列任意一个镜像站配置内容粘贴进去。根据自己的操作系统及其版本,选择下面的一个镜像源的配置内容粘贴进sources.list即可。(1) Debian1. 11.x - bullseye① 阿里云# 阿里镜像源 deb https://mirrors.aliyun.com/debian bullseye main non-free contrib deb https://mirrors.aliyun.com/debian bullseye-updates main non-free contrib deb https://mirrors.aliyun.com/debian bullseye-proposed-updates main non-free contrib deb https://mirrors.aliyun.com/debian bullseye-backports main non-free contrib deb https://mirrors.aliyun.com/debian bullseye-backports-sloppy main non-free contrib deb-src https://mirrors.aliyun.com/debian bullseye main non-free contrib deb-src https://mirrors.aliyun.com/debian bullseye-updates main non-free contrib deb-src https://mirrors.aliyun.com/debian bullseye-proposed-updates main non-free contrib deb-src https://mirrors.aliyun.com/debian bullseye-backports main non-free contrib deb-src https://mirrors.aliyun.com/debian bullseye-backports-sloppy main non-free contrib # 阿里安全更新镜像源 deb https://mirrors.aliyun.com/debian-security bullseye-security main non-free contrib deb-src https://mirrors.aliyun.com/debian-security bullseye-security main non-free contrib如果你的服务器是阿里云的Debian服务器并且想要给它配置镜像源的话,可以配置阿里云的内网镜像源,这样在阿里云服务器上下载软件会更快:# 阿里云内网镜像源 deb http://mirrors.cloud.aliyuncs.com/debian bullseye main non-free contrib deb http://mirrors.cloud.aliyuncs.com/debian bullseye-updates main non-free contrib deb http://mirrors.cloud.aliyuncs.com/debian bullseye-proposed-updates main non-free contrib deb http://mirrors.cloud.aliyuncs.com/debian bullseye-backports main non-free contrib deb http://mirrors.cloud.aliyuncs.com/debian bullseye-backports-sloppy main non-free contrib deb-src http://mirrors.cloud.aliyuncs.com/debian bullseye main non-free contrib deb-src http://mirrors.cloud.aliyuncs.com/debian bullseye-updates main non-free contrib deb-src http://mirrors.cloud.aliyuncs.com/debian bullseye-proposed-updates main non-free contrib deb-src http://mirrors.cloud.aliyuncs.com/debian bullseye-backports main non-free contrib deb-src http://mirrors.cloud.aliyuncs.com/debian bullseye-backports-sloppy main non-free contrib # 阿里云内网安全更新镜像源 deb http://mirrors.cloud.aliyuncs.com/debian-security bullseye-security main non-free contrib deb-src http://mirrors.cloud.aliyuncs.com/debian-security bullseye-security main non-free contrib事实上就是改个镜像源地址为内网镜像地址就OK了!② 腾讯云# 腾讯镜像源 deb https://mirrors.cloud.tencent.com/debian bullseye main non-free contrib deb https://mirrors.cloud.tencent.com/debian bullseye-updates main non-free contrib deb https://mirrors.cloud.tencent.com/debian bullseye-proposed-updates main non-free contrib deb https://mirrors.cloud.tencent.com/debian bullseye-backports main non-free contrib deb https://mirrors.cloud.tencent.com/debian bullseye-backports-sloppy main non-free contrib deb-src https://mirrors.cloud.tencent.com/debian bullseye main non-free contrib deb-src https://mirrors.cloud.tencent.com/debian bullseye-updates main non-free contrib deb-src https://mirrors.cloud.tencent.com/debian bullseye-proposed-updates main non-free contrib deb-src https://mirrors.cloud.tencent.com/debian bullseye-backports main non-free contrib deb-src https://mirrors.cloud.tencent.com/debian bullseye-backports-sloppy main non-free contrib # 腾讯安全更新镜像源 deb https://mirrors.cloud.tencent.com/debian-security bullseye-security main non-free contrib deb-src https://mirrors.cloud.tencent.com/debian-security bullseye-security main non-free contrib同样地,如果你的服务器是腾讯云的Debian服务器并且想要给它配置镜像源的话,可以配置腾讯云的内网镜像源,这样在腾讯云服务器上下载软件会更快:# 腾讯云内网镜像源 deb http://mirrors.tencentyun.com/debian bullseye main non-free contrib deb http://mirrors.tencentyun.com/debian bullseye-updates main non-free contrib deb http://mirrors.tencentyun.com/debian bullseye-proposed-updates main non-free contrib deb http://mirrors.tencentyun.com/debian bullseye-backports main non-free contrib deb http://mirrors.tencentyun.com/debian bullseye-backports-sloppy main non-free contrib deb-src http://mirrors.tencentyun.com/debian bullseye main non-free contrib deb-src http://mirrors.tencentyun.com/debian bullseye-updates main non-free contrib deb-src http://mirrors.tencentyun.com/debian bullseye-proposed-updates main non-free contrib deb-src http://mirrors.tencentyun.com/debian bullseye-backports main non-free contrib deb-src http://mirrors.tencentyun.com/debian bullseye-backports-sloppy main non-free contrib # 腾讯云内网安全更新镜像源 deb http://mirrors.tencentyun.com/debian-security bullseye-security main non-free contrib deb-src http://mirrors.tencentyun.com/debian-security bullseye-security main non-free contrib③ 清华大学镜像站# 清华大学镜像源 deb https://mirrors.tuna.tsinghua.edu.cn/debian bullseye main non-free contrib deb https://mirrors.tuna.tsinghua.edu.cn/debian bullseye-updates main non-free contrib deb https://mirrors.tuna.tsinghua.edu.cn/debian bullseye-proposed-updates main non-free contrib deb https://mirrors.tuna.tsinghua.edu.cn/debian bullseye-backports main non-free contrib deb https://mirrors.tuna.tsinghua.edu.cn/debian bullseye-backports-sloppy main non-free contrib deb-src https://mirrors.tuna.tsinghua.edu.cn/debian bullseye main non-free contrib deb-src https://mirrors.tuna.tsinghua.edu.cn/debian bullseye-updates main non-free contrib deb-src https://mirrors.tuna.tsinghua.edu.cn/debian bullseye-proposed-updates main non-free contrib deb-src https://mirrors.tuna.tsinghua.edu.cn/debian bullseye-backports main non-free contrib deb-src https://mirrors.tuna.tsinghua.edu.cn/debian bullseye-backports-sloppy main non-free contrib # 清华大学安全更新镜像源 deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bullseye-security main non-free contrib deb-src https://mirrors.tuna.tsinghua.edu.cn/debian-security bullseye-security main non-free contrib④ 中科大镜像站# 中科大镜像源 deb http://mirrors.ustc.edu.cn/debian bullseye main non-free contrib deb http://mirrors.ustc.edu.cn/debian bullseye-updates main non-free contrib deb http://mirrors.ustc.edu.cn/debian bullseye-proposed-updates main non-free contrib deb http://mirrors.ustc.edu.cn/debian bullseye-backports main non-free contrib deb http://mirrors.ustc.edu.cn/debian bullseye-backports-sloppy main non-free contrib deb-src http://mirrors.ustc.edu.cn/debian bullseye main non-free contrib deb-src http://mirrors.ustc.edu.cn/debian bullseye-updates main non-free contrib deb-src http://mirrors.ustc.edu.cn/debian bullseye-proposed-updates main non-free contrib deb-src http://mirrors.ustc.edu.cn/debian bullseye-backports main non-free contrib deb-src http://mirrors.ustc.edu.cn/debian bullseye-backports-sloppy main non-free contrib # 中科大安全更新镜像源 deb http://mirrors.ustc.edu.cn/debian-security bullseye-security main non-free contrib deb-src http://mirrors.ustc.edu.cn/debian-security bullseye-security main non-free contrib⑤ 官方镜像对于位于国外的服务器,推荐使用官方的镜像。# 官方软件源 deb http://deb.debian.org/debian bullseye main non-free contrib deb http://deb.debian.org/debian bullseye-updates main non-free contrib deb http://deb.debian.org/debian bullseye-proposed-updates main non-free contrib deb http://deb.debian.org/debian bullseye-backports main non-free contrib deb http://deb.debian.org/debian bullseye-backports-sloppy main non-free contrib deb-src http://deb.debian.org/debian bullseye main non-free contrib deb-src http://deb.debian.org/debian bullseye-updates main non-free contrib deb-src http://deb.debian.org/debian bullseye-proposed-updates main non-free contrib deb-src http://deb.debian.org/debian bullseye-backports main non-free contrib deb-src http://deb.debian.org/debian bullseye-backports-sloppy main non-free contrib # 官方安全更新源 deb http://deb.debian.org/debian-security bullseye-security main non-free contrib deb-src http://deb.debian.org/debian-security bullseye-security main non-free contrib2. 10.x - buster① 阿里云# 阿里镜像源 deb https://mirrors.aliyun.com/debian buster main non-free contrib deb https://mirrors.aliyun.com/debian buster-updates main non-free contrib deb https://mirrors.aliyun.com/debian buster-proposed-updates main non-free contrib deb https://mirrors.aliyun.com/debian buster-backports main non-free contrib deb https://mirrors.aliyun.com/debian buster-backports-sloppy main non-free contrib deb-src https://mirrors.aliyun.com/debian buster main non-free contrib deb-src https://mirrors.aliyun.com/debian buster-updates main non-free contrib deb-src https://mirrors.aliyun.com/debian buster-proposed-updates main non-free contrib deb-src https://mirrors.aliyun.com/debian buster-backports main non-free contrib deb-src https://mirrors.aliyun.com/debian buster-backports-sloppy main non-free contrib # 阿里安全更新镜像源 deb https://mirrors.aliyun.com/debian-security buster/updates main non-free contrib deb-src https://mirrors.aliyun.com/debian-security buster/updates main non-free contrib同样地,阿里云的Debian服务器建议使用阿里云的内网镜像源,这样速度更快:# 阿里内网镜像源 deb http://mirrors.cloud.aliyuncs.com/debian buster main non-free contrib deb http://mirrors.cloud.aliyuncs.com/debian buster-updates main non-free contrib deb http://mirrors.cloud.aliyuncs.com/debian buster-proposed-updates main non-free contrib deb http://mirrors.cloud.aliyuncs.com/debian buster-backports main non-free contrib deb http://mirrors.cloud.aliyuncs.com/debian buster-backports-sloppy main non-free contrib deb-src http://mirrors.cloud.aliyuncs.com/debian buster main non-free contrib deb-src http://mirrors.cloud.aliyuncs.com/debian buster-updates main non-free contrib deb-src http://mirrors.cloud.aliyuncs.com/debian buster-proposed-updates main non-free contrib deb-src http://mirrors.cloud.aliyuncs.com/debian buster-backports main non-free contrib deb-src http://mirrors.cloud.aliyuncs.com/debian buster-backports-sloppy main non-free contrib # 阿里内网安全更新镜像源 deb http://mirrors.cloud.aliyuncs.com/debian-security buster/updates main non-free contrib deb-src http://mirrors.cloud.aliyuncs.com/debian-security buster/updates main non-free contrib② 腾讯云# 腾讯镜像源 deb https://mirrors.cloud.tencent.com/debian buster main non-free contrib deb https://mirrors.cloud.tencent.com/debian buster-updates main non-free contrib deb https://mirrors.cloud.tencent.com/debian buster-proposed-updates main non-free contrib deb https://mirrors.cloud.tencent.com/debian buster-backports main non-free contrib deb https://mirrors.cloud.tencent.com/debian buster-backports-sloppy main non-free contrib deb-src https://mirrors.cloud.tencent.com/debian buster main non-free contrib deb-src https://mirrors.cloud.tencent.com/debian buster-updates main non-free contrib deb-src https://mirrors.cloud.tencent.com/debian buster-proposed-updates main non-free contrib deb-src https://mirrors.cloud.tencent.com/debian buster-backports main non-free contrib deb-src https://mirrors.cloud.tencent.com/debian buster-backports-sloppy main non-free contrib # 腾讯安全更新镜像源 deb https://mirrors.cloud.tencent.com/debian-security buster/updates main non-free contrib deb-src https://mirrors.cloud.tencent.com/debian-security buster/updates main non-free contrib同样地,若是腾讯云的Debian服务器则建议使用腾讯云的内网镜像源,这样速度更快:# 腾讯云内网镜像源 deb http://mirrors.tencentyun.com/debian buster main non-free contrib deb http://mirrors.tencentyun.com/debian buster-updates main non-free contrib deb http://mirrors.tencentyun.com/debian buster-proposed-updates main non-free contrib deb http://mirrors.tencentyun.com/debian buster-backports main non-free contrib deb http://mirrors.tencentyun.com/debian buster-backports-sloppy main non-free contrib deb-src http://mirrors.tencentyun.com/debian buster main non-free contrib deb-src http://mirrors.tencentyun.com/debian buster-updates main non-free contrib deb-src http://mirrors.tencentyun.com/debian buster-proposed-updates main non-free contrib deb-src http://mirrors.tencentyun.com/debian buster-backports main non-free contrib deb-src http://mirrors.tencentyun.com/debian buster-backports-sloppy main non-free contrib # 腾讯云内网安全更新镜像源 deb http://mirrors.tencentyun.com/debian-security buster/updates main non-free contrib deb-src http://mirrors.tencentyun.com/debian-security buster/updates main non-free contrib③ 清华大学镜像站# 清华大学镜像源 deb https://mirrors.tuna.tsinghua.edu.cn/debian buster main non-free contrib deb https://mirrors.tuna.tsinghua.edu.cn/debian buster-updates main non-free contrib deb https://mirrors.tuna.tsinghua.edu.cn/debian buster-proposed-updates main non-free contrib deb https://mirrors.tuna.tsinghua.edu.cn/debian buster-backports main non-free contrib deb https://mirrors.tuna.tsinghua.edu.cn/debian buster-backports-sloppy main non-free contrib deb-src https://mirrors.tuna.tsinghua.edu.cn/debian buster main non-free contrib deb-src https://mirrors.tuna.tsinghua.edu.cn/debian buster-updates main non-free contrib deb-src https://mirrors.tuna.tsinghua.edu.cn/debian buster-proposed-updates main non-free contrib deb-src https://mirrors.tuna.tsinghua.edu.cn/debian buster-backports main non-free contrib deb-src https://mirrors.tuna.tsinghua.edu.cn/debian buster-backports-sloppy main non-free contrib # 清华大学安全更新镜像源 deb https://mirrors.tuna.tsinghua.edu.cn/debian-security buster/updates main non-free contrib deb-src https://mirrors.tuna.tsinghua.edu.cn/debian-security buster/updates main non-free contrib④ 中科大镜像站# 中科大镜像源 deb http://mirrors.ustc.edu.cn/debian buster main non-free contrib deb http://mirrors.ustc.edu.cn/debian buster-updates main non-free contrib deb http://mirrors.ustc.edu.cn/debian buster-proposed-updates main non-free contrib deb http://mirrors.ustc.edu.cn/debian buster-backports main non-free contrib deb http://mirrors.ustc.edu.cn/debian buster-backports-sloppy main non-free contrib deb-src http://mirrors.ustc.edu.cn/debian buster main non-free contrib deb-src http://mirrors.ustc.edu.cn/debian buster-updates main non-free contrib deb-src http://mirrors.ustc.edu.cn/debian buster-proposed-updates main non-free contrib deb-src http://mirrors.ustc.edu.cn/debian buster-backports main non-free contrib deb-src http://mirrors.ustc.edu.cn/debian buster-backports-sloppy main non-free contrib # 中科大安全更新镜像源 deb http://mirrors.ustc.edu.cn/debian-security buster/updates main non-free contrib deb-src http://mirrors.ustc.edu.cn/debian-security buster/updates main non-free contrib⑤ 官方镜像对于位于国外的服务器,推荐使用官方的镜像。# 官方软件源 deb http://deb.debian.org/debian buster main non-free contrib deb http://deb.debian.org/debian buster-updates main non-free contrib deb http://deb.debian.org/debian buster-proposed-updates main non-free contrib deb http://deb.debian.org/debian buster-backports main non-free contrib deb http://deb.debian.org/debian buster-backports-sloppy main non-free contrib deb-src http://deb.debian.org/debian buster main non-free contrib deb-src http://deb.debian.org/debian buster-updates main non-free contrib deb-src http://deb.debian.org/debian buster-proposed-updates main non-free contrib deb-src http://deb.debian.org/debian buster-backports main non-free contrib deb-src http://deb.debian.org/debian buster-backports-sloppy main non-free contrib # 官方安全更新源 deb http://deb.debian.org/debian-security buster/updates main non-free contrib deb-src http://deb.debian.org/debian-security buster/updates main non-free contrib(2) DeepinDeepin没有特殊情况就建议使用自带的官方镜像源。1. 20.x - apricot① 官方# 官方镜像源 deb https://community-packages.deepin.com/deepin apricot main contrib non-free deb-src https://community-packages.deepin.com/deepin apricot main contrib non-free② 阿里云# 阿里镜像站 deb https://mirrors.aliyun.com/deepin apricot main contrib non-free deb-src https://mirrors.aliyun.com/deepin apricot main contrib non-free③ 腾讯云# 腾讯镜像站 deb https://mirrors.cloud.tencent.com/deepin apricot main contrib non-free deb-src https://mirrors.cloud.tencent.com/deepin apricot main contrib non-free④ 清华大学镜像站# 清华大学镜像站 deb https://mirror.tuna.tsinghua.edu.cn/deepin apricot main contrib non-free deb-src https://mirror.tuna.tsinghua.edu.cn/deepin apricot main contrib non-free⑤ 中科大镜像站# 中科大镜像站 deb http://mirrors.ustc.edu.cn/deepin apricot main contrib non-free deb-src http://mirrors.ustc.edu.cn/deepin apricot main contrib non-free(3) Ubuntu1. 22.04.1 LTS - jammy① 阿里云# 阿里镜像站 deb https://mirrors.aliyun.com/ubuntu jammy main multiverse restricted universe deb https://mirrors.aliyun.com/ubuntu jammy-backports main multiverse restricted universe deb https://mirrors.aliyun.com/ubuntu jammy-proposed main multiverse restricted universe deb https://mirrors.aliyun.com/ubuntu jammy-security main multiverse restricted universe deb https://mirrors.aliyun.com/ubuntu jammy-updates main multiverse restricted universe deb-src https://mirrors.aliyun.com/ubuntu jammy main multiverse restricted universe deb-src https://mirrors.aliyun.com/ubuntu jammy-backports main multiverse restricted universe deb-src https://mirrors.aliyun.com/ubuntu jammy-proposed main multiverse restricted universe deb-src https://mirrors.aliyun.com/ubuntu jammy-security main multiverse restricted universe deb-src https://mirrors.aliyun.com/ubuntu jammy-updates main multiverse restricted universe② 腾讯云# 腾讯镜像站 deb https://mirrors.cloud.tencent.com/ubuntu jammy main multiverse restricted universe deb https://mirrors.cloud.tencent.com/ubuntu jammy-backports main multiverse restricted universe deb https://mirrors.cloud.tencent.com/ubuntu jammy-proposed main multiverse restricted universe deb https://mirrors.cloud.tencent.com/ubuntu jammy-security main multiverse restricted universe deb https://mirrors.cloud.tencent.com/ubuntu jammy-updates main multiverse restricted universe deb-src https://mirrors.cloud.tencent.com/ubuntu jammy main multiverse restricted universe deb-src https://mirrors.cloud.tencent.com/ubuntu jammy-backports main multiverse restricted universe deb-src https://mirrors.cloud.tencent.com/ubuntu jammy-proposed main multiverse restricted universe deb-src https://mirrors.cloud.tencent.com/ubuntu jammy-security main multiverse restricted universe deb-src https://mirrors.cloud.tencent.com/ubuntu jammy-updates main multiverse restricted universe③ 清华大学镜像站# 清华大学镜像站 deb https://mirror.tuna.tsinghua.edu.cn/ubuntu jammy main multiverse restricted universe deb https://mirror.tuna.tsinghua.edu.cn/ubuntu jammy-backports main multiverse restricted universe deb https://mirror.tuna.tsinghua.edu.cn/ubuntu jammy-proposed main multiverse restricted universe deb https://mirror.tuna.tsinghua.edu.cn/ubuntu jammy-security main multiverse restricted universe deb https://mirror.tuna.tsinghua.edu.cn/ubuntu jammy-updates main multiverse restricted universe deb-src https://mirror.tuna.tsinghua.edu.cn/ubuntu jammy main multiverse restricted universe deb-src https://mirror.tuna.tsinghua.edu.cn/ubuntu jammy-backports main multiverse restricted universe deb-src https://mirror.tuna.tsinghua.edu.cn/ubuntu jammy-proposed main multiverse restricted universe deb-src https://mirror.tuna.tsinghua.edu.cn/ubuntu jammy-security main multiverse restricted universe deb-src https://mirror.tuna.tsinghua.edu.cn/ubuntu jammy-updates main multiverse restricted universe④ 中科大镜像站# 中科大镜像站 deb https://mirrors.ustc.edu.cn/ubuntu jammy main multiverse restricted universe deb https://mirrors.ustc.edu.cn/ubuntu jammy-backports main multiverse restricted universe deb https://mirrors.ustc.edu.cn/ubuntu jammy-proposed main multiverse restricted universe deb https://mirrors.ustc.edu.cn/ubuntu jammy-security main multiverse restricted universe deb https://mirrors.ustc.edu.cn/ubuntu jammy-updates main multiverse restricted universe deb-src https://mirrors.ustc.edu.cn/ubuntu jammy main multiverse restricted universe deb-src https://mirrors.ustc.edu.cn/ubuntu jammy-backports main multiverse restricted universe deb-src https://mirrors.ustc.edu.cn/ubuntu jammy-proposed main multiverse restricted universe deb-src https://mirrors.ustc.edu.cn/ubuntu jammy-security main multiverse restricted universe deb-src https://mirrors.ustc.edu.cn/ubuntu jammy-updates main multiverse restricted universe(4) LinuxMint在这里需要说明一下:Linux Mint的软件源配置和其它系统稍有不同,需要同时修改/etc/apt/sources.list和/etc/apt/sources.list.d/official-package-repositories.list这两个文件,首先可以将两个文件内容都清空或者全部注释掉,然后选择下列任意一个镜像站的对应配置内容粘贴进去。1. 21 - vanessa① 阿里云/etc/apt/sources.list内容:# 阿里镜像站 deb https://mirrors.aliyun.com/linuxmint-packages vanessa backport import main multiverse romeo universe upstream deb-src https://mirrors.aliyun.com/linuxmint-packages vanessa backport import main multiverse romeo universe upstream/etc/apt/sources.list.d/official-package-repositories.list内容:# 阿里镜像站 deb https://mirrors.aliyun.com/ubuntu jammy main multiverse restricted universe deb https://mirrors.aliyun.com/ubuntu jammy-backports main multiverse restricted universe deb https://mirrors.aliyun.com/ubuntu jammy-proposed main multiverse restricted universe deb https://mirrors.aliyun.com/ubuntu jammy-security main multiverse restricted universe deb https://mirrors.aliyun.com/ubuntu jammy-updates main multiverse restricted universe deb-src https://mirrors.aliyun.com/ubuntu jammy main multiverse restricted universe deb-src https://mirrors.aliyun.com/ubuntu jammy-backports main multiverse restricted universe deb-src https://mirrors.aliyun.com/ubuntu jammy-proposed main multiverse restricted universe deb-src https://mirrors.aliyun.com/ubuntu jammy-security main multiverse restricted universe deb-src https://mirrors.aliyun.com/ubuntu jammy-updates main multiverse restricted universe② 腾讯云/etc/apt/sources.list内容:# 腾讯镜像站 deb https://mirrors.cloud.tencent.com/linuxmint vanessa backport import main multiverse romeo universe upstream deb-src https://mirrors.cloud.tencent.com/linuxmint vanessa backport import main multiverse romeo universe upstream/etc/apt/sources.list.d/official-package-repositories.list内容:# 腾讯镜像站 deb https://mirrors.cloud.tencent.com/ubuntu jammy main multiverse restricted universe deb https://mirrors.cloud.tencent.com/ubuntu jammy-backports main multiverse restricted universe deb https://mirrors.cloud.tencent.com/ubuntu jammy-proposed main multiverse restricted universe deb https://mirrors.cloud.tencent.com/ubuntu jammy-security main multiverse restricted universe deb https://mirrors.cloud.tencent.com/ubuntu jammy-updates main multiverse restricted universe deb-src https://mirrors.cloud.tencent.com/ubuntu jammy main multiverse restricted universe deb-src https://mirrors.cloud.tencent.com/ubuntu jammy-backports main multiverse restricted universe deb-src https://mirrors.cloud.tencent.com/ubuntu jammy-proposed main multiverse restricted universe deb-src https://mirrors.cloud.tencent.com/ubuntu jammy-security main multiverse restricted universe deb-src https://mirrors.cloud.tencent.com/ubuntu jammy-updates main multiverse restricted universe③ 清华大学镜像站/etc/apt/sources.list内容:# 清华大学镜像站 deb https://mirror.tuna.tsinghua.edu.cn/linuxmint vanessa backport import main multiverse romeo universe upstream deb-src https://mirror.tuna.tsinghua.edu.cn/linuxmint vanessa backport import main multiverse romeo universe upstream/etc/apt/sources.list.d/official-package-repositories.list内容:# 清华大学镜像站 deb https://mirror.tuna.tsinghua.edu.cn/ubuntu jammy main multiverse restricted universe deb https://mirror.tuna.tsinghua.edu.cn/ubuntu jammy-backports main multiverse restricted universe deb https://mirror.tuna.tsinghua.edu.cn/ubuntu jammy-proposed main multiverse restricted universe deb https://mirror.tuna.tsinghua.edu.cn/ubuntu jammy-security main multiverse restricted universe deb https://mirror.tuna.tsinghua.edu.cn/ubuntu jammy-updates main multiverse restricted universe deb-src https://mirror.tuna.tsinghua.edu.cn/ubuntu jammy main multiverse restricted universe deb-src https://mirror.tuna.tsinghua.edu.cn/ubuntu jammy-backports main multiverse restricted universe deb-src https://mirror.tuna.tsinghua.edu.cn/ubuntu jammy-proposed main multiverse restricted universe deb-src https://mirror.tuna.tsinghua.edu.cn/ubuntu jammy-security main multiverse restricted universe deb-src https://mirror.tuna.tsinghua.edu.cn/ubuntu jammy-updates main multiverse restricted universe④ 中科大/etc/apt/sources.list内容:# 中科大镜像站 deb http://mirrors.ustc.edu.cn/linuxmint vanessa backport import main multiverse romeo universe upstream deb-src http://mirrors.ustc.edu.cn/linuxmint vanessa backport import main multiverse romeo universe upstream/etc/apt/sources.list.d/official-package-repositories.list内容:# 中科大镜像站 deb https://mirrors.ustc.edu.cn/ubuntu jammy main multiverse restricted universe deb https://mirrors.ustc.edu.cn/ubuntu jammy-backports main multiverse restricted universe deb https://mirrors.ustc.edu.cn/ubuntu jammy-proposed main multiverse restricted universe deb https://mirrors.ustc.edu.cn/ubuntu jammy-security main multiverse restricted universe deb https://mirrors.ustc.edu.cn/ubuntu jammy-updates main multiverse restricted universe deb-src https://mirrors.ustc.edu.cn/ubuntu jammy main multiverse restricted universe deb-src https://mirrors.ustc.edu.cn/ubuntu jammy-backports main multiverse restricted universe deb-src https://mirrors.ustc.edu.cn/ubuntu jammy-proposed main multiverse restricted universe deb-src https://mirrors.ustc.edu.cn/ubuntu jammy-security main multiverse restricted universe deb-src https://mirrors.ustc.edu.cn/ubuntu jammy-updates main multiverse restricted universe(5) Kali1. 2022.x - kali-rolling① 阿里云# 阿里镜像站 deb https://mirrors.aliyun.com/kali kali-rolling main non-free contrib deb-src https://mirrors.aliyun.com/kali kali-rolling main non-free contrib② 腾讯云# 腾讯镜像站 deb https://mirrors.cloud.tencent.com/kali kali-rolling main non-free contrib deb-src https://mirrors.cloud.tencent.com/kali kali-rolling main non-free contrib③ 清华大学镜像站# 清华大学镜像站 deb https://mirror.tuna.tsinghua.edu.cn/kali kali-rolling main non-free contrib deb-src https://mirror.tuna.tsinghua.edu.cn/kali kali-rolling main non-free contrib④ 中科大镜像站# 中科大镜像站 deb http://mirrors.ustc.edu.cn/kali kali-rolling main non-free contrib deb-src http://mirrors.ustc.edu.cn/kali kali-rolling main non-free contrib记得要以管理员身份才能编辑该文件!保存后执行命令sudo apt update以更新索引!3,软件源更新问题如果新安装的Linux,在根据上述操作配置软件源后,执行sudo apt update时报错如下:这是由于系统缺少软件包ca-certificates导致。解决办法是,先编辑软件源配置/etc/apt/sources.list文件,将其中所有的地址前面的https改成http,再执行下列命令:sudo apt update sudo apt install ca-certificates然后,再把/etc/apt/sources.list文件中的所有的地址前面的http重新改回https,并再次执行sudo apt update即可!参考:Debian软件仓库wiki:传送门
MongoDB是一个高性能的非关系型数据库,今天就来分享一下使用Docker部署MongoDB的方式。1,拉取镜像通过以下命令:docker pull mongo2,创建数据卷同样地,我们需要为其配置文件、数据以及集群元数据目录创建数据卷,因为这些数据较为重要,需要持久化到宿主机:docker volume create mongo-config docker volume create mongo-data docker volume create mongo-cluster这样就完成了具名数据卷的创建。3,编写配置文件按照上述命令完成数据卷配置之后,配置文件数据卷目录位于:/var/lib/docker/volumes/mongo-config/_data,进入这个目录,新建配置文件mongod.conf,配置文件为YAML格式,并自行按需加入配置。配置文件选项可以参考:官方文档如果不需要定义配置文件则可以不创建上述的mongo-config数据卷,同样地下面启动命令也不需要挂载配置目录。4,创建并启动容器通过下列命令:docker run -id --name=mongodb -v mongo-config:/etc/mongo -v mongo-data:/data/db -v mongo-cluster:/data/configdb -p 27017:27017 -e LANG=C.UTF-8 mongo mongod -f /etc/mongo/mongod.conf此时,容器就成功创建并运行了!上述参数意义如下:-v 用于挂载数据卷,可见配置文件数据卷mongo-config对应的是容器内/etc/mongo目录,因此末尾的容器启动命令中指定配置文件的位置和这里是对应的-p 暴露其默认端口27017-e 指定语言环境变量为C.UTF-8,防止中文乱码主要是注意最后的启动命令:mongod -f /etc/mongo/mongod.conf这里指定的配置文件位置和前面-v挂载配置文件数据卷的时候对应的容器内目录是一致的。如果说你没有指定配置文件,且未创建mongo-config数据卷,则启动命令如下:docker run -id --name=mongodb -v mongo-data:/data/db -v mongo-cluster:/data/configdb -p 27017:27017 -e LANG=C.UTF-8 mongo5,登入MongoDB除了远程连接之外,还可以直接在服务器终端上进行连接:docker exec -it mongodb mongosh
在网上购买的服务器有的没有安装中文环境,这样除了显示的系统提示是英文的之外,还会导致中文乱码。今天以Debian系统为例,分享一下Linux上中文语言及其环境配置。1,安装语言包首先我们需要安装locales这个软件包:sudo apt install locales2,配置语言环境执行下列命令配置语言环境:sudo dpkg-reconfigure locales出现配置界面如下:在这里选择要安装的语言环境,通常我们不需要全部选择安装,选择需要的即可。语言项非常多,通过鼠标滚轮、PageUp或者PageDown可以上下翻页,有的终端也可以用Home和End跳转到开头或者结尾,上下键逐个移动光标,翻到最下面可以找到中文语言环境:按下空格即可选择,前面带星号(*)即被选中,通常中文选择如图的zh_CN.GBK GBK和zh_CN.UTF-8 UTF-8这两个即可,最后按下回车确定。然后就是默认语言设置:在这里推荐Linux环境下使用zh_CN.UTF-8这一项,回车确定,这样就配置完成了!然后注销重新登录,或者重连服务器,语言配置就生效了。3,配置后仍然不生效问题在绝大多数情况下,完成上述配置,注销或者重启后,或者是重新连接服务器后配置就生效了,无需再做下面的操作,少数情况不生效可以通过环境变量再设置一下即可。下面介绍locale命令并提供几个方案,大家视情况选择其一即可。(1) locale命令基本使用首先执行下列命令查看已安装的语言环境:locale -a然后就是查看当前系统语言环境变量配置:locale(2) 临时改变语言环境在终端中设定LANG环境变量即可,例如我要临时改变语言环境为C.UTF-8:export LANG=C.UTF-8这样会立即生效,但是重启或者重新登录后失效。(3) 永久改变当前用户语言环境进入用户目录,编辑.bashrc即可:cd ~ vim .bashrc例如永久改变我当前用户的语言环境为C.UTF-8,则在.bashrc文件末尾加入:export LANG=C.UTF-8(4) 永久改变系统全局语言环境在/etc/profile.d中增加一个set-lang.sh文件(可以自定义文件名),并在其中写上上述设定语言环境变量的命令即可。cd /etc/profile.d touch set-lang.sh chmod +x set-lang.sh比如说要设定系统语言环境为zh_CN.utf8,则编辑set-lang.sh内容如下并保存:#!/bin/bash export LANG=zh_CN.utf8重启即可。可见这几种方式虽然作用域不同,但是都是通过环境变量即可完成设定。5,Docker容器内乱码问题使用Docker容器的话配置locales还是很麻烦的,因此不建议在容器中使用上述方式,只需要设定容器内语言环境变量为C.UTF-8即可。若是自己制作镜像,在Dockerfile中加入:ENV LANG C.UTF-8若是创建一个容器,加上如下环境变量参数:-e LANG=C.UTF-8这样,容器内的中文就可以正常显示了!
在后端开发过程中,我们绕不开的就是数据结构设计以及关联的问题。然而在传统的单体架构的开发中,解决数据关联的问题并不难,通过关系型数据库中的关联查询功能,以及MyBatis的级联功能即可实现。但是在分布式微服务中,整个系统都被拆分成了一个个单独的模块,每个模块也都是使用的单独的数据库。这种情况下,又如何解决不同模块之间数据关联问题呢?事实上,分布式微服务是非常复杂的,无论是系统架构,还是数据结构设计,都没有一个统一的方案,因此根据实际情况进行确定即可,对于数据关联的跨库查询,事实上也有很多方法,在网上有如下思路:数据冗余法远程连接表数据复制使用非关系型数据库...今天,我就来分享一个简单的分布式微服务跨库查询操作,大家可以参考一下。我们还是从一对多,多对多的角度来解决这个问题。1,学习前需要了解在继续往下看之前,我想先介绍一下这次的示例中所使用的组件:Spring Cloud 2022.0.0和Spring Cloud Alibaba 2022.0.0.0-RC1MyBatis-Plus作为ORM框架Dynamic Datasource作为多数据源切换组件Nacos作为注册中心MySQL数据库OpenFeign远程调用因此,在往下看之前,需要先掌握上述这些组件的使用,本文不再赘述。之前都是使用MyBatis作为ORM框架,而MyBatis-Plus可以视作其升级版,省去了我们写繁杂的Mapper.xml文件的步骤,上手特别简单。MyBatis-Plus官方文档:传送门Dynamic Datasource官方文档:传送门单体架构中数据结构关联的操作方式:传送门Maven多模块项目配置:传送门Jackson注解过滤字段:传送门将上述所有前置内容掌握之后,再来往下看最好。2,跨库操作解决思路我们从数据的联系形式,即一对多和多对多这两个角度依次进行分析。(1) 一对多一对多事实上比较好解决,这里我使用字段冗余 + 远程调用的方式解决。这里以订单(Order)和用户(User)为例,订单对象中通常要包含用户关系,一个用户会产生多个订单,因此用户和订单构成了一对多的关系。在单体架构中,我们很容易想到设计成这样:这样,在查询订单的时候,可以通过关联查询的方式得到用户字段信息。但是在分布式微服务中,用户和订单模块被拆分开来,两者的数据库也分开了,无法使用关联查询了,怎么办呢?这时,我们可以在订单类中,冗余一个userId字段,可以直接从数据库取出,再通过远程调用的方式调用用户模块,用这个userId去得到用户对象,最后组装即可。这样,订单服务查询订单对象,可以分为如下几步:先直接从数据库取出订单对象,这样上述Order类中的id、name和userId都可以直接从数据库取出然后拿着这个userId的值去远程调用用户服务得到用户对象,填充到user字段与此同时,我们还可以注意一下细节:将Order对象返回给前端时,可以过滤掉冗余字段userId,节省流量,通过Jackson注解可以实现前端若要将Order对象作为参数传递给后端,则无需带着user字段内容,这样前端传来后可以直接丢进数据库,并且更加简洁(2) 多对多我们知道,多对多通常是以搭桥表方式实现关联。在此我们增加一个商品类(Product),和订单类构成多对多关系,即需要查询一个订单中包含的所有商品,还需要查询这个商品被哪些订单包含。在传统单体架构中,我们如下设计:那么在分布式微服务中,数据库分开的情况下,这个搭桥表order_product放在哪呢?可以将其单独放在一个数据库中,这个数据库在这里称之为搭桥表数据库。这样,比如说订单服务查询订单的时候,可以分为如下几步:先直接从订单数据库查询出订单信息,这样Order类中的id、name和userId就得到了然后从搭桥表数据库去查询和这个订单关联的商品id,这样就得到了一个商品id列表用这个商品id列表去远程调用商品服务,查询到每个id对应的商品对象得到一个商品对象列表将商品列表组装到Order中的products字段中那么反过来,商品服务也是通过一样的方式得到订单列表并组装。可见,这两个多对多模块,有下列细节:需要用到两个数据库,因此需要配置多数据源两者需要暴露批量id查询的接口,但是批量id查询的时候,要注意死循环问题,这个我们在下面代码中具体来看(3) 总结可见上述解决数据关联的方式,都是要通过远程调用的方式来实现,这样符合微服务中职责单一原则,不过缺点是网络性能不是很好。但是,这种方式解决规模不是特别复杂的项目已经足够了。整体的类图和数据库如下:那么下面,我们就来实现一下。3,代码实现(1) 环境配置在写代码之前,我们先要在本地搭建并运行好MySQL和Nacos注册中心,这里我已经在本地通过Docker的方式部署好了,大家可以先自行部署。然后在这里,整个工程模块组织如下:存放全部实体类的模块,是普通Maven项目,被其它模块依赖远程调用层,是普通Maven项目,其它服务模块依赖这个模块进行远程调用订单服务模块,是Spring Boot项目商品服务模块,是Spring Boot项目用户服务模块,是Spring Boot项目我们知道服务提供者和服务消费者在整个分布式微服务中是非常相对的概念,而服务消费者是需要进行远程调用的,这样每个服务消费者都要引入OpenFeign依赖并注入等等,因此我们可以单独把所有的消费者的远程调用层feignclient抽离出来,作为这个远程调用模块。在最后我会给出项目的仓库的地址,大家可以在示例仓库中自行查看每个模块的配置文件和依赖配置。(2) 数据库的初始化在MySQL中通过以下命令,创建如下数据库:create database `db_order`; create database `db_product`; create database `db_user`; create database `db_bridge`;上述db_bridge就是专门存放搭桥表的数据库。然后依次初始化三个数据库。db_order数据库:-- 订单数据库 drop table if exists `order_info`; create table `order_info` ( `id` int unsigned auto_increment, `name` varchar(16) not null, `user_id` int unsigned not null, primary key (`id`) ) engine = InnoDB default charset = utf8mb4; -- 测试数据 insert into `order_info` (`name`, `user_id`) values ('订单1', 1), -- id:1~4 ('订单2', 1), ('订单3', 2), ('订单4', 3);db_product数据库:-- 商品数据库 drop table if exists `product`; create table `product` ( `id` int unsigned auto_increment, `name` varchar(32) not null, primary key (`id`) ) engine = InnoDB default charset = utf8mb4; -- 初始化测试数据 insert into `product` (`name`) values ('商品1'), -- id:1~3 ('商品2'), ('商品3');db_user数据库:-- 用户数据库 drop table if exists `user`; create table `user` ( `id` int unsigned auto_increment, `username` varchar(16) not null, primary key (`id`) ) engine = InnoDB default charset = utf8mb4; -- 初始化数据 insert into `user` (`username`) values ('dev'), -- id:1~3 ('test'), ('admin');db_bridge数据库:-- 多对多关联记录数据库 drop table if exists `order_product`; -- 订单-商品多对多关联表 create table `order_product` ( `order_id` int unsigned, `product_id` int unsigned, primary key (`order_id`, `product_id`) ) engine = InnoDB default charset = utf8mb4; -- 初始化测试数据 insert into `order_product` values (1, 1), (1, 2), (2, 1), (2, 2), (3, 2), (3, 3), (4, 1), (4, 2), (4, 3);(3) 实体类的定义所有的实体类存放在db-entity模块中。首先我们还是定义一个结果类Result<T>专用于返回给前端:package com.gitee.swsk33.dbentity.model; import lombok.Data; import java.io.Serializable; /** * 返回给前端的结果对象 */ @Data public class Result<T> implements Serializable { /** * 是否操作成功 */ private boolean success; /** * 消息 */ private String message; /** * 数据 */ private T data; /** * 设定成功 * * @param message 消息 */ public void setResultSuccess(String message) { this.success = true; this.message = message; this.data = null; } /** * 设定成功 * * @param message 消息 * @param data 数据 */ public void setResultSuccess(String message, T data) { this.success = true; this.message = message; this.data = data; } /** * 设定失败 * * @param message 消息 */ public void setResultFailed(String message) { this.success = false; this.message = message; this.data = null; } }然后就是数据库对象了。1. 用户类package com.gitee.swsk33.dbentity.dataobject; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import lombok.Data; /** * 用户类 */ @Data public class User { /** * 用户id */ @TableId(type = IdType.AUTO) private Integer id; /** * 用户名 */ private String username; }2. 商品类package com.gitee.swsk33.dbentity.dataobject; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import lombok.Data; import java.util.List; /** * 商品表 */ @Data public class Product { /** * 商品id */ @TableId(type = IdType.AUTO) private Integer id; /** * 商品名 */ private String name; /** * 所有购买了这个商品的订单(需组装) */ @TableField(exist = false) private List<Order> orders; }可见这里用了@TableField注解将orders字段标注为非数据库字段,因为这个字段是我们后续要手动组装的多对多字段。3. 订单类package com.gitee.swsk33.dbentity.dataobject; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; import java.util.List; /** * 订单类 */ @Data @JsonIgnoreProperties(allowSetters = true, value = {"userId"}) @TableName("order_info") public class Order { /** * 订单id */ @TableId(type = IdType.AUTO) private Integer id; /** * 订单名 */ private String name; /** * 关联用户id(一对多冗余字段,不返回给前端,但是前端作为参数传递) */ private Integer userId; /** * 关联用户(需组装) */ @TableField(exist = false) private User user; /** * 这个订单中所包含的商品(需组装) */ @TableField(exist = false) private List<Product> products; }可见这里使用了@JsonIgnoreProperties过滤掉了冗余字段userId不返回给前端。(4) 各个服务模块基本上每个服务模块仍然是Spring Boot的四层架构中的三层,即dao、service和api。因此这里只讲关键性的东西,其余细节可以在文末示例仓库中看代码。来看订单模块,定义数据库操作层OrderDAO如下:package com.gitee.swsk33.dborder.dao; import com.baomidou.dynamic.datasource.annotation.DS; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.gitee.swsk33.dbentity.dataobject.Order; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Select; import java.util.List; public interface OrderDAO extends BaseMapper<Order> { /** * 添加关联记录 * * @param orderId 订单id * @param productId 商品id * @return 增加记录条数 */ @Insert("insert into `order_product` values (#{orderId}, #{productId})") @DS("bridge") int insertRecord(int orderId, int productId); /** * 根据订单id查询其对应的所有商品id列表 * * @param orderId 订单id * @return 商品id列表 */ @Select("select `product_id` from `order_product` where `order_id` = #{orderId}") @DS("bridge") List<Integer> selectProductIds(int orderId); /** * 删除和某个订单关联的商品id记录 * * @param orderId 订单id * @return 删除记录数 */ @Delete("delete from `order_product` where `order_id` = #{orderId}") @DS("bridge") int deleteProductIds(int orderId); }由于继承了MyBatis-Plus的BaseMapper,因此基本的增删改查这里不需要写了,所以这里只需要写对搭桥表数据库中的操作,比如说获取这个订单中包含的商品id列表等等,也可见这里只获取id或者是传入id为参数对搭桥表进行增删查操作,并且这些方法标注了@DS切换数据源查询。再来看Service层代码:package com.gitee.swsk33.dborder.service.impl; import com.gitee.swsk33.dbentity.dataobject.Order; import com.gitee.swsk33.dbentity.dataobject.Product; import com.gitee.swsk33.dbentity.dataobject.User; import com.gitee.swsk33.dbentity.model.Result; import com.gitee.swsk33.dbfeign.feignclient.ProductClient; import com.gitee.swsk33.dbfeign.feignclient.UserClient; import com.gitee.swsk33.dborder.dao.OrderDAO; import com.gitee.swsk33.dborder.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.List; @Component public class OrderServiceImpl implements OrderService { @Autowired private OrderDAO orderDAO; @Autowired private UserClient userClient; @Autowired private ProductClient productClient; @Override public Result<Void> add(Order order) { Result<Void> result = new Result<>(); // 先直接插入 if (orderDAO.insert(order) < 1) { result.setResultFailed("插入失败!"); return result; } // 插入后,添加与之关联的商品多对多记录 for (Product each : order.getProducts()) { orderDAO.insertRecord(order.getId(), each.getId()); } result.setResultSuccess("插入完成!"); return result; } @Override public Result<Void> delete(int id) { Result<Void> result = new Result<>(); // 先直接删除 if (orderDAO.deleteById(id) < 1) { result.setResultFailed("删除失败!"); return result; } // 然后删除关联部分 orderDAO.deleteProductIds(id); result.setResultSuccess("删除成功!"); return result; } @Override public Result<Order> getById(int id) { Result<Order> result = new Result<>(); // 先查询订单 Order getOrder = orderDAO.selectById(id); if (getOrder == null) { result.setResultFailed("查询失败!"); return result; } // 远程调用用户模块,组装订单中的用户对象字段(一对多关联查询) User getUser = userClient.getById(getOrder.getUserId()).getData(); if (getUser == null) { result.setResultFailed("查询失败!"); return result; } getOrder.setUser(getUser); // 远程调用商品模块,组装订单中关联的商品列表(多对多关联查询) List<Integer> productIds = orderDAO.selectProductIds(id); getOrder.setProducts(productClient.getByBatchId(productIds).getData()); result.setResultSuccess("查询成功!", getOrder); return result; } @Override public Result<List<Order>> getByBatchId(List<Integer> ids) { Result<List<Order>> result = new Result<>(); // 先批量查询 List<Order> getOrders = orderDAO.selectBatchIds(ids); // 组装其中的用户对象字段 for (Order each : getOrders) { each.setUser(userClient.getById(each.getUserId()).getData()); } // 由于批量查询目前专门提供给内部模块作为多对多关联查询时远程调用,因此这里不再对每个对象进行多对多查询,否则会陷入死循环 result.setResultSuccess("查询完成!", getOrders); return result; } }可以先看代码和注释,这里面已经写好了增删查记录的时候的流程和操作,包括查询多对多搭桥表中的id以及远程调用等等,远程调用代码这里不再赘述。然后,将这些服务暴露为接口即可,反过来商品服务模块也是基本一样的思路。上述有以下需要注意的地方:将搭桥表的操作,即增加、查询和删除这个订单包含的商品id的操作,定义在了OrderDAO中,反过来在商品服务中,ProductDAO中也需要定义增加、查询和删除和这个商品所有关联的订单id的操作,具体可以看项目源码在服务中编写了getByBatchId这个方法专用于模块远程调用查询多对多的对象,但是可见在这个方法中批量查询时,没有继续组装每个对象中包含的多对多对象,防止死循环对于远程调用,需要注意的是远程调用层的代码和这些服务不在一个模块中,因此在模块主类上标注@EnableFeignClients注解启用远程调用功能时,还需要指定需要用到的远程调用的FeignClient类,否则会注入失败:所有模块写完后,启动并测试接口,来看一下效果:4,总结事实上,解决分布式微服务的跨库增删改查操作,有很多的方式,这里只是提供一个思路,大家可以适当采纳,不能说这里的方案就是最优雅、性能最好的,还需要根据实际情况考虑。示例仓库地址Apifox测试配置
有些时候我们需要用电脑但是不在家,或者是干脆懒得带笔记本?其实我们可以把系统塞进移动硬盘,然后在公用电脑(当然是没锁BIOS的情况下)或者借别人的电脑时仍然使用自己的系统和文件。对于Windows系统,可以使用WinToGo,通常只需要使用WTG辅助工具即可,一键式操作很方便。但是如果是Linux系统怎么做呢?其实很简单。就像正常一样安装Linux即可,只不过是把安装位置变成了移动硬盘而已。1,准备工欲善其事,必先利其器。我们需要准备以下东西:一个移动硬盘/U盘:最好是性能好的,建议用固态移动硬盘,不建议使用U盘,否则会卡死你虚拟机软件VMWare:用于把Linux安装到移动硬盘Linux安装镜像:今天以Deepin 20为例2,安装步骤其实,任何Linux操作系统,安装的步骤都是差不多的,无外乎就是什么设定系统用户名、设定安装位置、等待安装完毕就行了。只不过这次,我们要把Linux的安装位置变成移动硬盘,这里以Deepin为例,别的都大同小异。(1) 把移动硬盘清空可以使用DiskGenius等等分区软件把移动硬盘所有分区先删掉(先备份好文件),以便于后续的系统安装。由于是UEFI启动模式安装,因此还需要确保硬盘是GPT分区表。(2) 使用VMWare新建一个虚拟机用于安装我们的系统到移动硬盘使用虚拟机把系统安装到移动硬盘是个方便的方式。新建虚拟机:选择稍后按照系统:操作系统选Linux,发行版随便,但是系统位数得和你的要安装的系统位数对应,我这里选64位因为我要安装Deepin的64位:自定义位置和名称:这个不管:然后完成:然后编辑虚拟机设置:把硬盘移除掉(我们不需要虚拟硬盘):设置USB兼容性为3.1:设置光盘映像为我们的安装镜像:然后点击上方选项-高级-固件类型设置为UEFI,完成:(3) 启动虚拟机接入硬盘准备安装系统现在插上你的移动硬盘,开启虚拟机:现在已经进入安装选择界面了,但是不要急着进安装程序:这时先不要进安装程序,找到右下角移动硬盘图标,将其连接到虚拟机:好了,现在硬盘才接入虚拟机了,才能进入安装程序。(4) 开始安装语言、用户等等其它设定和往常一样:硬盘分区这里点击手动安装,可见这里只有一个硬盘就是我们的移动硬盘:点击硬盘列表最右边的添加分区按钮:先添加一个启动分区,也就是EFI分区,大小最小即可:再继续在剩余的可用空间点击添加按钮,添加一个交换分区,建议大小8GB即可:剩下的空间就全部作为系统分区,当然你也可以自己分配:文件系统ext4,挂载点/,大小拉满:然后下一步:继续安装:然后就开始安装了!等待安装结束即可。(5) 安装完毕,关闭虚拟机安装完成之后,安装程序会提示你可以拔出介质,这个时候就可以直接点击关闭虚拟机了。现在你的移动硬盘已经装进了系统,插在别的电脑上,开机时就可以从这个移动硬盘启动进入你自己的口袋Linux了!通常电脑开机的时候按F2可以进主板BIOS,按F12可以直接选择启动设备,开机之前把你的硬盘插上去选择启动设备为你的硬盘即可进入系统。不同电脑或者主板按键不一样,具体可以自己查。3,总结其实可见把一个系统装进移动硬盘,再装进口袋并不是一件难事。我们主要是借助了虚拟机,把移动硬盘接入后,启动安装程序把系统安装到移动硬盘,而实际安装过程和我们平时几乎一样。
泰拉瑞亚是一个非常好玩的沙盒游戏,以冒险作为主要主题。不过带上同伴一起披荆斩棘,比起单打独斗会有着更多的乐趣。而通过Steam联机有时会出现不稳定的情况,因此搭建泰拉瑞亚游戏服务器也是很好的选择。今天就以在Debain系统上搭建泰拉瑞亚服务器为例。1,下载泰拉瑞亚服务端文件首先进入游戏官网:传送门划到页面最底下,点击这个PC Dedicated Server链接即可下载最新版的服务端程序:如果说想下载历史版本服务端,可以去Wiki页面:或者在备用地址下载,提取码2333。注意游戏版本要和服务端版本一致!否则会导致无法进入服务器。下载后得到的是一个压缩包,解压后会有三个文件夹,对应着三个不同系统的服务端:这里我们只需要把Linux文件夹中的全部文件上传到我们的服务器上面即可。至于服务器的购买就不再赘述了。2,启动服务端为了使游戏服务器能够在后台运行,我们可以借助screen命令把服务端进程放在后台运行,先安装screen并创建一个新的窗口:# 安装 apt install screen # 创建一个名为terraria的窗口 screen -S terrariascreen命令的使用就不再赘述了,非常简单。我这里把上述Linux文件夹中服务端程序上传到了服务器的/root/ter目录中,先使用cd命令进入这个目录,然后依次执行以下命令赋予权限并启动:chmod +x ./TerrariaServer.bin.x86_64 ./TerrariaServer.bin.x86_64可见TerrariaServer.bin.x86_64这个文件就是Linux服务端的主程序文件,运行它即可。这时会让你选择世界,但是这里还没有世界,因此输入n创建世界:选择世界大小,1-3分别对应小中大世界:然后选择难度,1-4分别对应简单、专家、大师和旅行难度:选择世界类型,1-3分别对应随机、腐化和猩红:然后输入世界名:输入种子,可以留空:此时等待生成世界:世界生成完成,就会回到选择世界界面,输入数字即可选择刚刚创建的世界:这里输入1回车,然后会要你设定最大玩家数量,可以输入8:然后设定端口,默认7777:然后设定是否开启转发,通常打开,输入y:然后设定房间密码:这时服务器就启动了!在这里输入save指令可以保存世界,exit指令保存并关闭服务器。服务端通常放在screen的窗口中,下次连接服务器时想进入这个游戏服务端控制台就使用screen -r命令。再次启动服务器,只需要运行服务端主程序文件TerrariaServer.bin.x86_64,选择世界,设定端口密码等等即可。3,配置文件与无交互运行这里大家也发现了:每次启动服务端,就需要设定房间端口号那些东西,很麻烦。那有没有办法启动服务器就开启房间呢?当然可以!借助配置文件即可。先在服务端文件夹(服务端主程序文件所在文件夹)创建一个文本文件作为配置文件并编辑:# 先进入服务端文件夹 touch config.txt vim config.txt配置文件中配置的格式如下:配置项=值常用配置如下:world 指定世界存档文件的位置,当且仅当指定了这个配置的时候,服务端启动时就会直接加载世界存档文件,读取配置并直接开启房间,而无需我们再每次输入端口号密码等,世界存档文件扩展名为.wld,文件名和路径都可以自定义,若存档文件不存在会自动创建maxplayers 设定最大玩家数port 设定房间端口号,推荐就使用默认的7777即可password 设定房间密码motd 设定进入房间时的消息worldpath 指定创建新世界的时候,世界存档文件存放的文件夹(注意这个配置要指定文件夹,以/结尾)language 设定语言,指定为zh-Hans可以设定为中文upnp 通常设定为1打开端口转发可见只要配置了world配置,就可以直接启动房间而无需每次手动输入配置,其余配置大家自行配置。如果说world指定的存档不存在则会自动创建,除此之外你还可以把自己电脑上的存档wld文件放到服务器上面并将其路径指定为world配置。电脑上泰拉瑞亚游戏世界存档位于:C:\Users\你的用户名\Documents\My Games\Terraria\Worlds目录下。如果想要指定自动创建时世界的难度类型等等,还可以加入以下配置:autocreate 设定自动创建时世界大小,值为1-3,分别对应小中大世界seed 设定自动创建时世界的种子,随机的话就不写该配置worldname 自动创建世界时的世界名difficulty 设定自动创建世界时的难度,值为0-3,分别对应简单,专家,大师和旅行难度除此之外,#开头的内容即视为注释。这里有一个配置模板,大家可以复制并修改:# 房间选项 world=/root/terraria/world/main.wld worldpath=/root/terraria/world/ maxplayers=8 port=7777 password=123456 motd=Welcome! language=zh-Hans upnp=1 # 自动创建选项 autocreate=2 worldname=World difficulty=2创建完成配置文件,启动服务端时也需要加上-config命令行参数指定配置文件位置:./TerrariaServer.bin.x86_64 -config ./config.txt可见在-config参数后指定配置文件路径即可。这样,启动时就会自动读取我们的配置并直接开启房间了!因此平时也推荐使用配置文件的形式。4,泰拉瑞亚服务端的Docker版不使用容器化部署服务端的话可以不看这一节!除了上述我们直接搭建启动服务端的方式之外,方便起见我还制作了简单的泰拉瑞亚服务端Docker镜像,可以直接拉取并部署:docker pull swsk33/terraria-server至于容器部署的方式和注意事项请查看:镜像仓库页5,总结可见搭建泰拉瑞亚服务端并不难,通过配置文件可以更加方便。参考链接:官方Wiki:传送门
Docker是我们常用的容器引擎,使用Docker来部署和管理我们的常用数据库例如MySQL是非常的方便的。不过使用Docker安装部署MySQL还是有一些需要注意的地方的。1,拉取MySQL镜像使用docker pull命令即可拉取:docker pull mysql2,创建数据卷MySQL作为数据库,其中通常存放我们的用户数据,都是很重要的,因此在容器化部署MySQL时,将MySQL的数据文件等等持久化是非常有必要的。这里我们使用具名挂载的方式挂载MySQL容器的数据卷,方便管理。先创建三个数据卷,分别用于挂载并持久化MySQL的数据文件、配置文件和日志这三个目录:docker volume create mysql-data docker volume create mysql-config docker volume create mysql-log这样,我们就创建了三个数据卷,这三个数据卷分别被命名为mysql-data、mysql-config和mysql-log,大家也可以自行取名。3,创建MySQL容器通过以下命令创建MySQL容器:docker run -id --name=mysql -v mysql-config:/etc/mysql/conf.d -v mysql-log:/logs -v mysql-data:/var/lib/mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -e LANG=C.UTF-8 mysql这样,就完成了MySQL容器的创建和启动,上述命令的参数意义如下:-id 将MySQL容器挂在后台运行--name=mysql 将容器起名为mysql,大家可以自己起名,该参数可以省略-v mysql-config:/etc/mysql/conf.d 把MySQL容器中的配置文件目录挂载至上述创建的名为mysql-config的数据卷上面,还有两个-v挂载数据卷的参数同理-p 3306:3306 把容器的3306端口映射到宿主机的3306端口,这样才能从外网访问这台机器上的MySQL,若你的数据库只需要从本机访问,就可以去除这个参数-e MYSQL_ROOT_PASSWORD=123456 设置容器环境变量MYSQL_ROOT_PASSWORD的值为123456,这个环境变量表示MySQL的root用户的密码,一定要设置,这里设置了密码为123456,大家可以自定义-e LANG=C.UTF-8 设置容器的语言环境变量LANG值为C.UTF-8,这个必须要设置,否则容器内默认是英文环境,使得MySQL无法存放中文内容4,修改MySQL配置文件上述我们已经把创建了数据卷并具名挂载了容器内的MySQL配置目录,只需查看数据卷的位置并修改其中文件即可。查看数据卷mysql-config的位置:docker volume inspect mysql-config进入这个目录,可以看到里面有两个默认的MySQL配置文件,使用vim编辑即可,在里面加入自定义的配置。编辑完成记得重启容器:# 上述创建时容器名为mysql,若为自定义此处换为自己的 docker restart mysql5,第一次添加用户或者设置外网访问创建完成容器之后,我们可以先进入这个MySQL容器,并使用root用户连接,创建用户等等。先进入容器的shell:docker exec -it mysql bash这时终端就变成了容器内的终端,使用下列命令即可连接MySQL:mysql -u root -p密码就是上述创建容器时你设置的MYSQL_ROOT_PASSWORD容器环境变量的值。至于配置文件和用户添加等等常用操作这里不再赘述了,可以直接参考这篇博客的修改配置文件和添加用户等等部分:传送门
之前分享过在使用Vue-cli的时候怎么构建多页应用,如今随着Vite这样一个轻量级易用的构建工具的出现,越来越多的人也去尝试Vite了。在Vue-cli迁移到Vite的过程中,多多少少会有一些问题,多页应用的配置就是其中之一。1,认识Vite项目中页面加载和根组件挂载过程事实上,Vite项目中根组件加载过程和Vue-cli是差不多的,只不过目录结构有点小变化。上述目录和文件:1.vscode项目配置2.nodejs项目依赖库文件,平时需要加入.gitignore3.公共文件夹4.源码文件夹4.1.存放静态资源例如图片、音频等等4.2.存放Vue组件4.3.Vue根组件,这个大家应该很熟悉4.4. 入口文件,大家也耳熟能详了5. Git忽略文件6. 主页面模板文件7. 依赖配置文件,里面记录了依赖库8. README文件9. Vite项目的配置文件,和Vue-cli中的vue.config.js是一个作用可见,对比起Vue-cli项目,最大的不同就是原先在public文件夹中的index.html,现在是在项目根目录了!(1) Vite项目中路径和网站访问路径的对应关系首先我们要引入一个概念:根路径。在Vite工程中,默认情况下根路径就是我们的项目目录,即src文件夹所在的那个目录(注意是所在),在Vite中用/表示根路径。因此在这里/开头的路径就是绝对路径。通过在配置文件中指定root配置项的值可以自定义根路径的位置,不过平时可以不用修改。那么在Vite中,我们的根组件又是如何被加载的呢?在Vite工程中,当我们访问网站根路径的时候,事实上会访问到我们项目根路径上的index.html文件。没错,在Vite工程中,预览网页时访问网站的网址路径和我们工程路径是对应的。我们的Vite工程启动后,访问http://localhost:3000/,也就是访问我们的网址根路径的时候,Vite就会去项目中寻找/index.html并返回,也就是说我们会访问到我们项目中的/index.html上。同样地:访问http://localhost:3000/test/,Vite就会去项目目录中寻找/test/index.html并返回,即我们访问到的是/test/index.html文件访问http://localhost:3000/test/page/,Vite就会去项目目录中寻找/test/page/index.html并返回,即我们访问到的是/test/page/index.html文件可见,我们访问网页路径的时候,和项目路径是一一对应的,这和大多数静态服务器一样。同样地,在Vite中还有base配置项,这个配置项其实就和Vue-cli的publicPath是一个意思,就是开发或生产环境服务的公共基础路径,其默认值是/。通俗地讲,这个配置项表示我们外部访问的根路径是什么,也就是说我们在外部通过网址访问时访问什么路径,Vite就会去根路径寻找index.html。其默认值是/,也就是说正如上面讲的一样,我们访问网站根路径的时候,Vite就会让我们访问到项目根路径的index.html上面。以此类推就很简单了,如果我们配置base值为/path,那当我们访问http://localhost:3000/path/的时候,Vite才会去项目根路径找到index.html并返回。当我们配置base值为/path时,同样地:访问http://localhost:3000/path/test/,Vite就会去项目目录中寻找/test/index.html并返回,即我们访问到的是/test/index.html文件访问http://localhost:3000/path/test/page/,Vite就会去项目目录中寻找/test/page/index.html并返回,即我们访问到的是/test/page/index.html文件(2) 根组件挂载根组件的挂载和Vue-cli项目几乎一模一样,我们还是来看一下入口文件/src/main.js文件:import { createApp } from 'vue'; // 引入根组件 import App from './App.vue'; // 创建根组件实例并挂载至html文件中 createApp(App).mount('#app');重点仍然在于createApp这个函数,先是引入组件模板,然后通过createApp函数,会返回一个提供应用上下文的应用实例,可以链式调用。通俗地讲,先开始只是引入了Vue模板,但是仅仅引入模板,是不能将它和我们的html联系起来的。通过createApp,我们才创建了一个可以供上下文使用的实例,也就是说只有这个函数,才能创建一个可以显示渲染的实例。同样地,createApp函数后面也可以链式调用use函数引入其它插件。最后通过mount函数,把这个实例内容,即App.vue文件的内容挂载至网页某个节点上,这样才能显示。那为什么要挂载到id为app的节点上呢?可以打开/index.html文件看看:可见当我们访问网页根路径时,我们就访问了index.html这个网页,这时main.js被执行,其中createApp函数执行,并将App.vue的内容挂载(填充)至这个html的id为app的div中。这里App.vue的内容如下:访问网页,查看f12看看渲染结果:可见通过main.js,把App.vue中的内容挂载到了html文件中id为app的节点下。相信大家现在知道了Vite项目中,访问路径与项目路径的对应关系,以及一个页面的加载过程,知道了这些,配置出一个多页应用也是很简单的。2,多页应用配置同样地,多页应用中每一个页面也需要有它自己的html文件和入口js文件。假设我这里要配置两个子页面,我们可以先在项目根目录下创建两个文件夹存放两个子页面的html文件和入口js文件。我这里创建两个文件夹pageone和pagetwo,并将/index.html和/src/main.js各复制一份到这两个文件夹:每个子页面的入口js文件可以改名,但是html就不要改名了!保持index.html即可,否则访问时会导致找不到文件。可见我这里子页面1对应的入口文件是/pageone/pageone.js,对应的html是/pageone/index.html;子页面2对应的入口文件是/pagetwo/pagetwo.js,对应的html是/pagetwo/index.html。然后我们在src文件夹中新建page文件夹专门用于存放所有子页面的vue根组件及其子组件文件,在page中给每个页面创建个文件夹,并在文件夹中创建每个页面的根组件文件,如下:可见每个子页面的组件都放在/src/page中的一个文件夹中,便于管理。上述子页面1和子页面2的根组件分别是PageOne.vue和PageTwo.vue,两个文件内容如下:不过这里为什么要把vue文件和入口文件与html文件分开放呢?放一起不更方便吗?这里我觉得:Vite项目中src目录基本上存放的绝大多数都是vue组件文件,两者分开会使得结构清晰若把html和入口文件也放在src中,访问子页面的时候路径会很长,打包后也很难对应当然,这里也只是我的看法,大家可以根据自己的习惯进行组织,多页应用的配置的目录并非是死的,毕竟知道了页面加载的方式,我们也很容易配置多页应用了!好了,现在修改每个页面的html文件和入口文件,这里先修改子页面1的html文件,打开/pageone/index.html文件,找到script标签,把里面的src属性值改成子页面1对应的入口文件路径,也就是说让子页面1的html文件链接上其入口文件:注意这里src必须使用绝对路径!也就是/开头的路径,使用相对路径会导致无法加载。然后修改子页面1的入口文件,打开/pageone/pageone.js文件,import组件的部分改成引入子页面1对应的Vue组件文件的路径:好了,到此页面1就配置完成了!页面2也是同理。然后就可以访问我们的多页应用了!启动项目,访问http://localhost:3000/,如下:访问http://localhost:3000/pageone/,如下:访问http://localhost:3000/pagetwo/,如下:注意在这里预览页面时,网址路径末尾必须以/结尾!否则也会导致找不到页面。可见配置多页应用还是很简单的,如果仅仅是开发预览时配置多页应用,连配置文件都不用改。3,多页应用打包上述配置好多页应用之后,如果你这时执行打包构建命令,你会发现子页面文件并没有被包含到打包的结果中,因此我们还需要在配置文件中,配置一下打包解析,把每个子页面的html文件添加到打包解析中。打开Vite配置文件vite.config.js,在其中进行打包解析配置。先在配置文件中import一下resolve函数:import { resolve } from 'path';然后加入以下配置内容:build: { rollupOptions: { input: { // 配置所有页面路径,使得所有页面都会被打包 main: resolve(__dirname, 'index.html'), pageone: resolve(__dirname, 'pageone/index.html'), pagetwo: resolve(__dirname, 'pagetwo/index.html') } } }最终配置文件如下:可见在上述的input中,配置一下每个页面的html文件解析路径即可!其中input对象里面,属性名可以自定义,属性值则是要解析的页面路径,通过resolve函数。resolve函数的参数是填入多个路径,最终这个函数会把路径拼接起来。__dirname表示的是vite.config.js这个文件所在的路径。在这里vite.config.js位于/,因此__dirname的值就是/,对于上述resolve(__dirname, 'pagetwo/index.html'),最终拼接出来的路径就是/pagetwo/index.html,这样就成功地找到了子页面2的html文件,打包的时候就会被包含。需要注意的是,__dirname表示的是vite.config.js这个文件本身所在的路径而不是根路径! 因此即使你修改了配置项root为别的值,即你修改了根路径位置,这个__dirname的值仍然是vite.config.js这个文件所在的路径,这里需要注意一下。再次执行打包命令npm run build,发现所有页面都被包含了:同样地,如果配置了多页应用,就不能够把base配置为相对路径./了!否则会出现问题。4,配置打包后页面文件到后端服务器dist文件夹中的内容就可以放到后端服务器中去了!这里以Spring Boot为例,我们把dist文件夹中所有内容放到Spring Boot工程的src/main/resources/static目录下,即Spring Boot项目默认的静态资源目录下:Spring Boot默认端口是8080,启动项目,访问http://127.0.0.1:8080/index.html,如下:访问http://127.0.0.1:8080/pageone/index.html,如下:访问http://127.0.0.1:8080/pagetwo/index.html,如下:可见打包后配置到后端访问路径是和Vite开发预览时的是不一样的,这里注意即可,后端配置打包后的静态页面,访问路径就必须指定到具体的html文件上。同样地,如果觉得访问路径太长或者不好看,也可以在Spring Boot中自定义Controller类进行自定义路由,这都是Spring Boot方面的内容了!这里就不再过多赘述,相信后端同学都能够信手拈来。参考:Vite多页面应用:传送门Vite基本配置:传送门示例仓库地址
在日常开发过程中,我们也离不开命令行终端工具,但是系统自带的cmd并不是很好用,而Git Bash也没那么顺手。因此我们通常会去寻找一些新的终端工具。除了Windows Terminal之外,这里我也非常推荐Tabby这个功能强大的终端。1,下载在官网即可下载:tabby.sh点击下载按钮转到Github页面进行下载:可见这个软件支持全平台,大家下载安装包安装即可,非常简单。2,基本配置点击右上角设置图标即可进入设置界面:(1) 添加右键菜单在应用选项卡中,把Shell集成打开即可:这样我们就可以在指定文件夹中快速打开Tabby而不需要每次都使用cd命令进入某个文件夹:(2) 字体在外观选项卡中配置字体大小、粗细、光标形状等等。字体配置建议不要手动输入字体名称,最好是直接粘贴字体名称进去,因为目前字体栏自动提示填充还是有点问题。(3) 终端设置在终端选项卡中可以配置回滚行数:以及推荐开启启动时开启一个终端窗口,关闭恢复终端标签页:(4) 配色方案在配色方案选项卡中,有很多种配色方案预设,选择自己喜欢的即可。(5) 窗口外观在窗口选项卡中,可以设定窗口透明度、框架样式等等:(6) 配置同步Tabby的全部设置都是可以云端同步的,使得我们多设备使用,首先去官网注册一个账户:点击左下角登录:选择一个登录方式,推荐微软账户或者Github:登录后,点击左下角设置,复制同步密钥:这个密钥就是你的账户的同步密钥。然后在Tabby的配置同步选项卡中,粘贴你的同步密钥:然后你的配置就会同步到云端了!以后换了设备,仍然可以把你的同步密钥粘贴进去以获取在线配置。3,配置其它终端在设置的配置和连接中,可以管理和新建终端预设。例如平时Git Bash、Msys2里面自带的Bash并不好用,因此可以用Tabby加载它们的环境以执行它们的命令,只需在这个选项卡中新建配置即可。下面就来讲解一下怎么在Tabby中配置Git和Msys2,使得我们可以用Tabby执行Git或者Msys2命令。(1) Git Bash如果你正确安装了Git,那么里面是会自带Git Bash的预设的,可以直接使用:不过如果Tabby没有检测到你安装了Git,则需要手动配置一下。点击上面新配置按钮,选择CMD (clink)作为模版:需要注意的是命令行这里,先把命令行中已有的内容清空,然后填入以下内容:'你的Git安装路径\\bin\\bash.exe' -l -i默认Git安装在C:\Program Files\Git。可见上述路径部分要用英文单引号'包围,并且路径里面的\要用\\表示,新建其它终端预设也一样。图标这里大家可以填这个表示Git的图标:<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="svg-inline--fa fa-git-alt fa-w-14 fa-3x" data-icon="git-alt" data-prefix="fab" focusable="false" role="img" viewBox="0 0 448 512"><path fill="#f05033" stroke="none" stroke-width="1" d="M439.55 236.05L244 40.45a28.87 28.87 0 0 0-40.81 0l-40.66 40.63 51.52 51.52c27.06-9.14 52.68 16.77 43.39 43.68l49.66 49.66c34.23-11.8 61.18 31 35.47 56.69-26.49 26.49-70.21-2.87-56-37.34L240.22 199v121.85c25.3 12.54 22.26 41.85 9.08 55a34.34 34.34 0 0 1-48.55 0c-17.57-17.6-11.07-46.91 11.25-56v-123c-20.8-8.51-24.6-30.74-18.64-45L142.57 101 8.45 235.14a28.86 28.86 0 0 0 0 40.81l195.61 195.6a28.86 28.86 0 0 0 40.8 0l194.69-194.69a28.86 28.86 0 0 0 0-40.81z"></path></svg>推荐大家把Git Bash作为默认终端。(2) Msys2同样地,新建配置,用CMD (clink)作为模版,在命令行里面填入如下命令行参数:'你的Msys2安装目录\\msys2_shell.cmd' -msys2 -defterm -here -no-start然后就可以新建一个Msys2的终端窗口了:配置其余终端大同小异。(3) 远程SSH除此之外,Tabby还可以配置连接远程服务器,并且自带文件传输功能,也是在这里配置远程服务器地址即可。远程连接后,点击右上角SFTP即可浏览远程主机的文件列表,然后单击或者双击下载远程主机文件,也可以上传本地文件到远程主机。还可以对文件右键 - Edit locally直接使用本地文本编辑器对远程主机的文件进行编辑,这样就不需要我们执行vim命令编辑远程主机上的文件了!与此同时,通过一点点配置,还可以在打开SFTP时让Tabby自动定位到终端的当前目录,参考:传送门可见Tabby不仅仅是一个命令行终端工具,还具备了Xshell和Xftp这样远程连接软件的常用功能,可见是非常方便的。
Redis作为我们最常用的内存数据库之一,在面对高并发环境时,也需要保持高可用性。因此,通常情况下我们需要配置Redis集群。Redis集群,通常是在多个服务器上部署Redis服务,每个服务器作为一个节点,所有的节点之间互相联系,数据互通,这样就构成了Redis集群。不过通常我们手上可能没有这么多服务器,不足以搭建集群,因此本文就在一台电脑上运行多个Redis进程为例,模拟集群搭建过程。Redis集群有三种模式,下面我就来一一介绍一下它们的原理和搭建方式。1,主从复制模式(1) 原理与过程主从复制模式是将多个节点中分为主节点和从属节点,通常主节点只有一台,从节点有很多台,主节点提供写入和读取功能,但是从属节点只提供读取功能。图中master就是主节点,而slave就是从属节点,通常我们向主节点写入数据之后,主节点就会同步到从节点中去。可见主从复制模式有以下优点:读写分离,职责明确高可用,如果从属节点挂掉一个,则整个集群仍然可用主从复制的数据同步过程如下:首先主节点启动,然后从属节点启动,从属节点会连接主节点并发送SYNC命令以请求同步主节点收到SYNC命令之后,就会执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的写入命令执行了BGSAVE之后,就向所有从属节点发送快照文件从属节点收到快照文件之后,会丢弃自己已有的所有旧数据并把收到的快照写入数据库之后,主节点会把缓冲区中的写命令发送给所有从节点实现从节点的增量同步(2) 部署主从复制集群这里我们搭建一个“一主二从”的集群,也就是一个主节点两个从属节点的集群,集群地址列表如下:节点地址端口主节点127.0.0.17000从属1127.0.0.17001从属2127.0.0.17002首先我们要安装好Redis,建议在Linux服务器上面进行搭建,确保redis-server,redis-cli和redis-sentinel命令可以正常使用。这里就不讲述Redis的安装了,对于基本安装和配置不明白的可以参考我之前写的Linux上Redis安装配置的博客。创建三个文件夹,里面分别存放我们三个节点的Redis配置文件,三个Redis节点的运行路径也对应着我们三个目录。我这里创建如下:我这里创建了7000、7001和7002文件夹,每个文件夹里面都有一个redis.conf表示每个节点的配置文件。7000里面是主节点配置,文件内容如下:# 主节点配置 # 端口号 port 7000 # 后台运行 daemonize yes # 密码 requirepass 12345678可见主节点配置和平常没什么特别的地方。7001和7002里面是从属节点配置,内容如下:# 从属1配置 # 端口号 port 7001 # 后台运行 daemonize yes # 密码 requirepass 12345678 # 指定该从属的主节点的地址和端口 replicaof 127.0.0.1 7000 # 主机密码 masterauth 12345678# 从属2配置 # 端口号 port 7002 # 后台运行 daemonize yes # 密码 requirepass 12345678 # 指定该从属的主节点的地址和端口 replicaof 127.0.0.1 7000 # 主机密码 masterauth 12345678可见从属节点中,需要配置replicaof指定其对应的主节点的地址和端口号,以及masterauth指定主节点的密码,若主节点没有配置密码就可以省略此项,否则主节点配置了密码但是不写masterauth的话,就会导致从属节点同步数据失败!我们通常最好是设定全部节点密码都一样。然后依次使用cd命令进入三个文件夹并执行命令:redis-server ./redis.conf这样,三个节点就启动了!我们可以用redis-cli命令连接一下我们任意一个节点,比如这里我连接主节点:redis-cli -h 127.0.0.1 -p 7000-h后面是地址,-p后面是端口。连上后使用auth 密码认证,然后就可以执行命令了!先执行以下命令查看节点信息:info replication现在在主节点写一个数据试试:set a b然后退出连接一个从属节点:redis-cli -h 127.0.0.1 -p 7001在从属节点获取刚刚写的数据:get a这说明主节点写入数据后,成功地同步到了从属节点。(3) 主从复制模式的问题事实上,主从复制模式的缺陷也很明显:假设主节点挂了,我们需要手动去设置一个从属节点变为主节点并修改其它节点配置。因此,主从复制模式目前用的不是说特别广泛。但是为什么这里还要学习主从复制模式呢?因为下面的模式很大程度上都基于主从复制模式。我们接着来看。2,哨兵模式(1) 原理和过程哨兵模式事实上是上述主从复制模式的一种扩展,也就是说在主从复制模式的基础上,增加哨兵节点以监视所有节点的情况,假设主节点挂掉了,哨兵节点会从所有从属节点中选举一个节点作为新的主节点,并修改其余从属节点的配置。哨兵节点自身不会提供任何数据的读写存储功能,仅仅是负责监视所有的节点。哨兵不仅会监视所有复制数据存储读写的主从节点,还会互相监视。通常哨兵节点也会有好几个,不过当其中一个哨兵监视到主节点挂掉时,系统并不会马上进行选举,因为这仅仅是一个哨兵看到主节点挂掉,这时称之为主观下线。而当其它哨兵也发现主节点挂掉时,并且发现主节点挂掉的哨兵达到一定值时,哨兵之间才会进行选举过程,这时称之为客观下线。那要多少个哨兵发现主机挂掉才会触发选举呢?这个是可以配置的,我们下面来看。哨兵模式工作过程:每个哨兵进程以每秒钟一次的频率向其它所有节点发送PING命令如果一个节点距离最后一次有效回复时间间隔超过阈值,则这个节点会被这个哨兵标记为主观下线(SDOWN),这个阈值可以通过哨兵节点配置文件中的down-after-milliseconds配置,单位ms当主节点被任意一个哨兵标记为SDOWN,则正在监视主节点的所有哨兵会以每秒一次的频率确认主服务器的确进入了SDOWN状态当有足够数量的哨兵在指定的时间范围内确认主节点进入了SDOWN,则主节点会被标记为客观下线(ODOWN),进入故障切换(failover)过程开始选举,哨兵节点选举出新的主节点后,会以发布订阅模式通知其余从属节点修改配置文件指向新的主节点可见哨兵模式是一个自动化版的主从复制模式。(2) 哨兵模式集群搭建和配置在这里我们搭建一个“一主二从三哨兵”集群,如下:节点地址端口主节点127.0.0.17000从属1127.0.0.17001从属2127.0.0.17002哨兵1127.0.0.18000哨兵2127.0.0.18001哨兵3127.0.0.18002上述我们已经启动了“一主二从”集群,这里方便起见我们只需再创建三个文件夹存放三个哨兵的配置文件并在其中启动哨兵进程即可(哨兵模式中主从节点搭建配置和上述完全一样)。现在我这里目录如下:然后我贴出上述第一个哨兵的配置:# 哨兵1配置 # 端口 port 8000 # 被监视主机 sentinel monitor mymaster 127.0.0.1 7000 1 # 后台运行 daemonize yes # 连接主机密码 sentinel auth-pass mymaster 12345678这里sentinel monitor表示指定集群中主节点的地址和端口。这个配置有四个参数意义如下:mymaster 自定义的主节点名称,同一个集群中所有哨兵的主节点名称应当相同127.0.0.1 主节点地址7000 主节点端口1 表示触发客观下线的哨兵节点数,这里表示只要有一个哨兵标记主节点为主观下线,就触发选举然后sentinel auth-pass用于指定主节点密码,后面mymaster表示主节点名称,12345678就是主节点密码。其余哨兵节点配置我不再一一列出,只需复制上述配置文件然后修改里面的端口号配置,其余不变即可。当然注意集群中所有主从节点的密码要全部一样!否则可能出现同步和选举上的问题。主从节点都还在运行,这时我们依次使用cd命令进入三个哨兵配置文件夹执行下面命令启动哨兵进程(平时需要先启动主节点,再启动从属节点,最后启动哨兵节点,注意这个顺序):redis-sentinel ./sentinel.conf可见这里哨兵进程需要用redis-sentinel命令启动,后面参数也是接的配置文件路径。这时,整个哨兵模式集群搭建完毕!大家可以这个时候手动停掉主节点,然后依次连接从属节点执行info replication查看谁被选举成了新的主节点,这里我就不再演示了。(3) Spring Boot配置连接Redis哨兵模式集群我们很多时候使用spring-data-redis来连接Redis,不过这里连接哨兵集群和平时配置就有所不同了,我这里配置如下:# 主节点名称 spring.redis.sentinel.master=mymaster # 哨兵节点列表,用英文逗号隔开 spring.redis.sentinel.nodes=127.0.0.1:8000,127.0.0.1:8001,127.0.0.1:8002 # 主节点的密码 spring.redis.password=12345678这里主节点名称就是上面哨兵配置中我们定义的mymaster,然后配置所有哨兵节点,格式为地址:端口,多个用英文逗号隔开,然后在配置主节点密码(注意是主节点密码)。这里我们只需配置哨兵节点地址即可,项目启动后会自动通过哨兵节点找到主节点并连接,进而执行读写操作。3,Cluster模式这也是官方推荐的模式,是新版本Redis支持的一种集群模式。(1) 原理和过程在上述哨兵模式中,已经实现了高可用和读写分离。但是我们也可见每个节点都要储存一份完整的数据,这样很浪费内存。因此Redis官方推出了Cluster模式,这种模式下每个节点不会储存完整的内容,但是节点直接相互连通,所有节点内容加起来才是完整的内容。上述每个节点可能储存一部分内容,但是不论某个内容存放在哪个节点,我们都可以通过任意一个节点访问到,因为它们之间互相连通。那么,在整个集群中又是如何存放数据的呢?事实上,搭建集群的时候,Redis会根据节点数量先分配主从节点,然后根据主节点数量平均分配整个空间。Redis会先把整个集群所使用的储存空间分为一定数量的等分,这个等分就叫做哈希槽(hash slot)。Redis集群中有16384个哈希槽,那么假设集群中有三个主节点分别是A、B和C,每个主节点对应一个从属节点A1、B1和C1,那么主节点会被分配槽位如下:A包含从0-5460哈希槽位B包含从5461-10922哈希槽位C包含从10923-16383哈希槽位那我们存放数据时,这个数据到底会放到哪个槽位中去?事实上,Redis也有这样的一个算法:存入数据时,就会对存入的键计算CRC16,然后拿计算出来的值对16384取模得到的结果,就是这个数据的槽位。同样地,从属节点也会和上面一样同步主节点的内容。假设现在主节点B挂掉了,是不是意味着5461-10922槽位不能再储存数据了呢?当然不是。这时其对应的从属节点B1会被自动地提升为主节点。(2) Cluster模式集群搭建和配置为了保证高可用性,Redis要求Cluster模式至少需要3个主节点,而每个主节点至少要配备一个从属节点,因此我们至少需要6个节点才能搭建起来一个Redis的Cluster集群,所以说实际情况下对我们服务器数量有所要求。节点如下:节点地址端口节点1127.0.0.17000节点2127.0.0.17001节点3127.0.0.17002节点4127.0.0.17003节点5127.0.0.17004节点6127.0.0.17005同样地,新建6个文件夹,里面分别存放每个节点的配置文件:这里放出第一个节点配置:# 节点1配置 # 端口号 port 7000 # 后台运行 daemonize yes # 开启集群模式 cluster-enabled yes # 配置当集群中有一台机器宕机时集群保持可用 cluster-require-full-coverage no # 设置密码 requirepass 12345678 # 其它集群服务节点密码 masterauth 12345678对于Cluster模式,我们每个节点都要配置cluster-enabled yes表示打开集群模式,然后注意配置所有节点密码一样!并配置masterauth选项表示该节点访问其余节点时的密码,否则也会导致节点之间互相连通时发生错误。其余的节点配置也可以拷贝这个文件只修改里面的端口配置即可。然后也是依次使用cd命令进入每个文件夹并依次执行启动命令:redis-server ./redis.conf启动之后,这时所有的节点仍然还是都单独运行的,我们还是需要用redis-cli命令将它们连结起来,才能完成集群搭建。命令格式如下:redis-cli --cluster create 节点1地址:节点1端口 节点2地址:节点2端口 ... --cluster-replicas 1 -a 密码--cluster-replicas 1表示每个主节点分配1个从节点,因此6个节点会被计算为3主3从。如果集群都没有设置密码,可以省略-a参数。我这里命令如下:redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 --cluster-replicas 1 -a 12345678注意实际情况下指定节点一定要指定节点服务器的外网ip而不是域名或者内网ip!否则可能会连结集群失败。还有就是我们上述节点端口是7000、7001...等等,这个是我们设定的客户端访问端口,而集群中节点互相连通访问的端口不是这个,而对应的是17000、17001...等等,也就是在客户端访问端口上加10000(外网访问端口和节点之间交流的端口总是相差10000),因此部署在服务器上时也要对应开放节点之间交流端口的防火墙。然后会出现这些信息,输入yes确认:出现这些信息则成功:现在,我们可以用redis-cli连接集群:redis-cli -h 127.0.0.1 -p 7000 -c -a 12345678其中-c表示以集群模式连接,-a指定密码,这里要说明的是集群有密码并且以集群模式连接时一定要用-a指定连接密码,否则如果连接后再用auth输密码会导致重定向失败!连接成功,写入数据试一下:可见连接7000端口节点并写入数据,结果重定向到端口为7002的节点写入,这是因为通过上述算法对我们存入的键进行CRC16再取模运算之后得到结果15495,这个槽位位于第三个节点也就是7002端口节点,读取也是一样,如果你连接7000端口节点去取刚刚存入的键,那么它也会重定向到对应位置取得到这个键的值。通过这样重定向实现各个节点连通。注意如果想重新配置集群,需要停止每个节点并把每个节点生成的这些文件删掉再执行上述步骤!注意这个nodes.conf是集群节点自动生成的配置,用于记录节点之间的信息,它默认生成在当前运行目录,和数据文件dump.rdb在一起,如果你配置了dir指定了Redis的数据存放目录,则会生成到你指定的数据存放目录中去。平时不需要手动修改,这个文件的文件名默认是nodes.conf,可以通过cluster-config-file配置项自定义。(3) Spring Boot连接Cluster模式集群同样还是使用spring-data-redis来连接Redis,配置如下:# 所有集群节点列表,用英文逗号隔开 spring.redis.cluster.nodes=127.0.0.1:7000,127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003,127.0.0.1:7004,127.0.0.1:7005 # 主机的密码 spring.redis.password=12345678可见这里只用配置所有节点地址端口和密码即可。(4) Redisson连接Cluster集群踩坑今天搭建了个外网服务器集群,并在一个Spring Boot项目中如上进行配置,这个项目使用了Redisson作为分布式锁功能。结果配置之后项目无法启动,并报错:nested exception is org.redisson.client.RedisConnectionException: Not all slots covered! Only 7647 slots are available. Set checkSlotsCoverage = false to avoid this check.我寻思着我的集群已经正确配置了啊,并且配置过程中没有任何报错。为什么这里提示集群没有覆盖全部哈希槽呢?事实上,这是因为Redisson无法连接所有的节点导致。上面我们知道了:Redis以集群模式启动时会生成一个nodes.conf文件记录集群信息,我打开一个节点的这个文件一看,发现问题所在:搭建集群时生成的nodes.conf文件内容也很明显:用于记录其余各个节点的id和地址。但是这个里面有个包含myself的行,显然是记录该节点自己的信息的,但是自己的地址记录的是内网地址,这时导致我们项目从外网获得一个内网地址然后无法连接。解决方法很显然,就是先停止所有节点,并依次打开每个节点生成的nodes.conf,找到myself那一行,将其内网地址改为该节点服务器的外网地址,保存,重新启动节点即可。由于这里已经生成了nodes.conf文件记录了节点信息,因此再次启动所有节点它们之间就可以再次构成集群,不需要再像第一次创建集群的时候使用redis-cli命令了!再次启动项目,就不会出错了!整个文章参考:Redis官方集群搭建文档:传送门Spring Data Redis配置文档:哨兵模式:传送门Cluster模式:传送门
在Vue项目开发中,我们常常会导入一些外部的模块,或者是自己写模块导入。但是模块多了,一个个地导入很显然不是一个好办法,因此我们可以批量导入。1,前置基础知识 - JavaScript模块化编程在以往开发普通网页程序时,我们直接使用script标签引入了js文件即可调用其函数,但是在vue中你发现就不行了,因为vue也是使用了模块化编程标准。通常我们可以把一个封装了很多要复用的函数和变量的js文件称作模块,模块必须暴露(导出)其中的变量、函数才能被外部导入并调用其中的函数。现在常见的模块标准有两个:CommonJS和ES6。在这里我们着重讲解ES6模块。(1) export语句 - 暴露函数和变量使外部调用现在我们自己创建一个js文件表示我们自己的模块,里面封装一些常常复用的函数、变量等等,就需要使用export暴露出来。// 导出常量a export const a = 'test'; // 导出函数myPrint export function myPrint(msg) { console.log('[]' + msg); }可见在定义变量/函数时在前面加上export即可。除此之外,还可以批量导出:const a = 'test'; function myPrint(msg) { console.log('[]' + msg); } // 导出常量a和函数myPrint export { a, myPrint };(2) import语句 - 导入变量/函数并使用上面封装好了函数、变量等等并导出了,但是还是不能直接使用的。需要在要使用的地方进行导入操作:// 导入函数myPrint和常量a import { a, myPrint } from './mymodule.js'; // 使用导入的变量和函数 console.log(a); myPrint('msg');这样就可以使用了。可见import语法如下:import { 变量1/函数1, 变量2/函数2, 变量3/函数3, ... } from 'js文件路径';需要注意,导入的变量和函数一次可以有多个,用大括号括起来,并且导入的变量/函数名必须和模块中导出的变量/函数名一致!(3) import * as xxx语句 - 全部导入对于需要导入很多的模块,我们import后面需要写一长串的导入的变量和函数,因此我们还可以一次性全部导入。对于上面的mymodule里面导出了一个变量和一个函数,在此我们可以一次性全部导入:// 一次性全部导入该模块的内容并命名为my import * as my from './mymodules.js'; // 调用模块属性a console.log(my.a); // 调用模块函数myPrint my.myPrint('hhh');可见,用import * as 名字 from '模块路径'的方式不仅可以方便地导入一个模块js文件的所有内容,还可以自己定义一个名字以方便调用。(4) export default - 默认导出上述的导出方式其实是命名式导出,其它文件导入时的变量/函数名必须和模块中导出的变量/函数名一致。但是有时候不知道我们要导入的js文件中的函数/变量名怎么办呢?可以使用默认导出,即export default语句,例如默认导出一个函数:// 默认导出myPrint函数 export default function myPrint(msg) { console.log('[]' + msg); }默认导出后,导入时可以自行命名:// 导入文件中默认导出的函数/变量并命名为p import p from './mymodule.js'; //使用 p('msg');可见这里没有使用大括号,并且导入时可以自行命名,不需要和原模块中函数名一样。注意,一个js文件只能有一个默认导出!不能多次默认导出!因此,一个有很多变量和函数的复用模块,我们可以这么写:export default { // 变量a a: 'a', // 函数myPrint myPrint() { console.log('aa'); } }调用时:import m from './mymodule.js'; //使用 console.log(m.a); m.myPrint();很显然,这里是直接默认导出了一个大的匿名JavaScript对象,并把变量、函数写在这个对象里面。然后再导入,调用其中变量/函数即可。其实在vue开发中,我们用到的很多外部JavaScript的库,都是这么做的。事实上,我们的Vue单文件组件原理不也是这样的?2,在Vite工程中批量导入js模块在之前的Vue-cli开发中,我们可以使用require.context()来实现批量导入。但是由于Vite是完全基于ES6模块的,因此就不能使用这种方式了。当然,官方也提供了实现批量导入的方式,用import.meta.glob或者import.meta.globEager函数实现。前者懒加载的导入,后者则为直接导入。今天主要讲解后者。例如我现在工程目录下src/assets/js下有三个js文件:现在要在根组件App.vue批量导入它们,则在<script>部分开头写:const importModules = import.meta.globEager('./assets/js/*.js');这样就导入了./assets/js下所有js模块文件。当然这种方式也可以匹配多级目录:const importModules = import.meta.globEager('./assets/js/**/*.js');这样就导入了./assets/js下所有js文件及其子目录下的所有js文件。在这里我们将其导入为一个变量importModules,这个变量里面到底是啥呢?我们在mounted函数里面打印一下看看:console.log(importModules);可见导入的是一个对象,这个对象中的每个属性即为模块路径,而对应的值中的default属性即为导入的模块本身,模块的函数变量等等都在里面。因此我们可以取导入的变量的每个属性下的default属性以调用我们的模块。举个例子,现在要调用模块one中的print函数:importModules['./assets/js/one.js'].default.print('hello');到这相信大家又发现问题了:每次调用批量导入的模块,就要用模块的完整路径去取,还需要带上default属性,及其不方便。我们能不能实现就用模块名(js文件名)去取出对应的模块呢?当然可以,事实上方法很多,我来讲一下我的思路。我们将上述存放导入模块的变量importModules中的每个属性名都给用字符串裁剪的方式替换成模块名,并将每个属性对应的值直接替换为它的default值不就行了吗?我们来试一下子:// 对批量导入存放模块的对象进行处理 // 先获取其全部属性 const keys = Object.keys(importModules); // 执行批量替换操作 for (let path of keys) { // 裁剪字符串方式得到路径中的文件名(无扩展名) let name = path.substring(path.lastIndexOf('/') + 1, path.lastIndexOf('.js')); // 对原对象执行添加新的属性并删除旧属性达到处理效果 importModules[name] = importModules[path].default; delete importModules[path]; }现在,我们就可以很方便地进行调用了!// 调用模块one的print importModules.one.print('hello'); // 打印模块two的name属性 console.log(importModules.two.name);3,在Vite工程中批量导入图片/音频等静态资源在Vite中如果是想要动态绑定图片音频视频等等,也是要用import语句的,同样地图片多了,不想一个个地import应当怎么做呢?事实上,我们还可以用import.meta.globEager批量导入静态资源例如图片音频等等。假设现在在src/assets/image下有很多图片。我们仍然可以用上面的方式批量导入:const importImages = import.meta.globEager('./assets/image/*');方式和上面一模一样,只不过这次导入的是静态资源,所以说上述importImages的default变成了对应资源的路径。打印看一看:同样地,我们可以把上述导入的每个default部分存起来再使用v-for批量呈现,试一下子,整个Vue文件代码如下:<template> <div class="app"> <!-- 然后就可以批量呈现了 --> <img v-for="item in images" :src="item" height="150"/> </div> </template> <script> const importImages = import.meta.globEager('./assets/image/*'); export default { data() { return { // 存放批量导入的图片 images: [] } }, mounted() { // 把导入的对象中每个default属性(对应实际导入的图片)取出来放到data中的变量images中去 for (let path in importImages) { this.images.push(importImages[path].default); } } } </script>效果:可见Vite中的批量导入非常方便,无论是模块还是资源都可以。对于静态资源,我们是不是还可以用这个批量导入机制,结合我之前写的判断各个资源加载完成的方法的文章,更加简单地实现网站资源加载检测并制作进度条呢?
在进行分布式系统开发时,我们通常会创建多个模块的工程项目。即每一个功能就是一个Spring Boot工程,作为一个个模块,然后这些模块都会有一个父模块,父模块通常没有代码只有一个pom.xml。今天就来分享一下Spring Boot如何创建一个多模块项目,以创建一个两个子模块的工程为例。1,创建父模块在IDEA中,创建一个Spring Boot项目,但是不勾选任何依赖:创建好之后,将父模块中除了pom.xml文件之外的全部文件删除:因为父模块只是做一个模块和依赖管理的作用,因此不需要代码。然后修改这个父模块的pom.xml文件,首先把节点、节点和全部删除:然后修改版本号为自己定义的(方便后续子模块指定父模块):然后修改父模块打包方式为pom,在其中加入如下语句即可:<packaging>pom</packaging>好的,到这里父模块修改就完成了!2,创建子模块在左边项目树中父模块位置右键新建Spring Boot工程:然后把子模块不需要的文件也删掉(只留pom.xml和src文件夹):修改该子模块的pom.xml文件,首先把子模块中的工件坐标改成和上述父模块一致:然后删除子模块的groupId节点,因为通常子模块继承父模块,子模块的组id是和父模块的一致的:ok,到此子模块创建并配置完成!此时这个子模块(工件名module-one)就继承了刚刚的父模块。然后以这个步骤再创建一个子模块module-two:最后整个工程就创建完成了!总共两个子模块。3,在父模块中指定子模块回到父模块的pom.xml文件,添加modules节点,在其中加入module节点以指定子模块:需要注意的是,module节点中的内容是子模块工程的文件夹名!所以通常规范起见子模块工程的文件夹名通常和它的组件名一致。然后在父模块文件夹中执行mvn clean package试试:可以看见构建打包成功,以及每个模块的构建时间。到此,多模块项目就创建完成了!4,子模块之间的互相引用在多模块项目中,子模块的互相引用也很方便。比如说上述module-one要调用module-two中的类,就直接把module-two的工件坐标加入到module-one的pom.xml的依赖部分即可!更新一下Maven工程,就可以在module-one中调用module-two的类了!不过这个时候运行工程是没有任何问题的,但是打包会出错:虽然module-one依赖了module-two,但是仍然会在打包module-one的时候,提示找不到module-two中的类。这是由于Spring Boot打包的模式问题,我们打开被依赖模块module-two的pom.xml文件找到最下面节点中,在spring-boot-maven-plugin插件部分中加入下面配置:<classifier>exec</classifier>最终如下:这个时候对父模块打包,就成功了!5,依赖管理多模块项目中模块变多了,依赖管理不当也会导致很多莫名其妙的问题。但是如果对每个模块分别管理依赖及其版本,会相当麻烦。(1) 共用的依赖假设上述module-one和module-two都需要依赖fastjson2,我们平常并不会依次在module-one和module-two中分别单独加入其依赖,而是直接在父模块pom.xml中指定,和平时一样,在父模块的pom.xml的dependencis节点中加入即可:这样,子模块中即使是不加入fastjson2依赖,也可以使用这个库了!因为子模块除了可以使用自己的依赖之外,还会向上查找父模块的依赖,也就是说,父模块的依赖是向下继承的,因此对于所有模块都要使用的依赖,我们可以写在父模块中。所以,两个模块都依赖于Spring Web话,也可以将两个模块的Spring Web依赖移至父模块。所以说父模块和子模块中,依赖也有着继承的关系!事实上,父模块的properties也是向下继承的。(2) 依赖版本管理假如现在module-one依赖于okhttps的4.0.0版本,而module-two依赖于commons-io的2.11.0版本,显然这时我们不适合再在父模块中加入了,还是各自加入对应依赖。目前因为只有两个模块,这么做看起来很合理。但是假设现在又多了module-three,module-four等等,它们也是分别依赖于okhttps与commons-io,那么我们又要分别在这两个模块中分别单独加入依赖。摸块依赖module-one和module-threeokhttpsmodule-two和module-fourcommons-io可能一开始没有问题,但是后面模块依赖版本需要更新,但是一个个地更新难免会有疏漏,导致部分模块虽然依赖一样但是版本不一致,最后整个系统也出现了莫名其妙地错误,还难以排查。就假设后面你把module-one的okhttps更新到了4.0.1版本,但是module-three的okhttps仍然是4.0.0版本,这样整个系统就可能出现问题甚至无法启动。尤其是模块变成十几个甚至上百个了,一个个地手动修改是几乎不可能的,要如何让每个模块使用的依赖版本统一呢?这时,我们就要借助dependencyManagement标签了!dependencyManagement用于管理依赖的版本,我们在父模块的pom.xml加入这个标签:<dependencyManagement> <dependencies> <dependency> <groupId>cn.zhxu</groupId> <artifactId>okhttps</artifactId> <version>4.0.0</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.11.0</version> </dependency> </dependencies> </dependencyManagement>然后,在子模块中,就可以把对应的依赖版本去掉了!可见这里,子模块中只需要引入对应依赖,而不需要指定版本了!因为子模块会向上查找父模块中dependencyManagement标签中对应依赖的版本。这样,就起到了统一管理版本的作用,只需要在父模块的dependencyManagement中修改对应依赖版本,子模块中对应依赖都会相应地使用这个版本。dependencyManagement的注意事项:dependencyManagement仅用于管理版本,而不会为自己以及子模块导入依赖,因此在dependencyManagement中声明依赖后,对应的子模块仍然需要在dependencies中加入依赖在pom.xml中dependencyManagement和dependencies同级,并且dependencyManagement中也需要有一个dependenciesdependencyManagement不仅可以管理子模块的依赖版本,也可以管理自身的依赖版本若不想让某个子模块使用父模块dependencyManagement的版本,那就在这个子模块的dependencies中声明对应版本(3) 总结总而言之,对于所有子模块都共用的依赖,我们只需在父模块的dependencies中引入这个依赖即可,而不需要再在子模块pom.xml中引入。而对于不是所有子模块都需要的依赖,而是部分子模块需要的,又要统一版本管理,这时除了在需要这个依赖的子模块中引入依赖之外,还需要在父模块中的dependencyManagement声明这个依赖及其版本,这时,可以去掉子模块中对应依赖的版本号,使其遵循父模块中声明的版本。示例仓库地址:简单多模块项目统一依赖管理的多模块项目
通常使用Linux操作系统我们会使用fcitx来作为中文拼音输入法。不过在新版的Debian 11/Deepin20中我们可以使用fcitx5了,新版的fcitx5有着更好的输入体验。除此之外,搜狗输入法linux版也在一直更新,已经适配最新的Linux系统,并且有着很好的中文输入体验,还是非常建议使用的。所以,今天就来分享一下fcitx5与新版搜狗输入法的安装与使用。1,卸载旧版输入法我们最好是先卸载旧版输入法,执行命令:sudo apt purge fcitx* ibus*2,安装fcitx5或者搜狗输入法由于fcitx5和搜狗输入法是不能共存的,因此大家只需要选择两者中的一个安装即可。我个人更习惯于使用搜狗输入法,大家根据自己的情况选择。下面我将分别介绍两者的安装。(1) 安装搜狗输入法在官网下载linux版搜狗输入法安装包:传送门需要注意的是官网上有新旧版本的两个下载链接:上面的图是新版本,下面的是旧版本,我们需要下载新版本的(图中4.0.0版本的),下载旧版本会导致安装失败。一般电脑下载x86版本,是一个deb格式的安装包文件。我下载的安装包文件名是:sogoupinyin_4.0.0.1605_amd64.deb,然后在安装包所在目录打开终端,执行命令安装:sudo dpkg -i sogoupinyin_4.0.0.1605_amd64.deb安装可能会提示安装出现问题,不用担心,再执行下列命令即可:sudo apt install -f等待安装完成,重启电脑,然后按Ctrl + 空格即可使用搜狗输入法。(2) 安装fcitx5安装也很简单,执行命令即可:sudo apt install fcitx5 fcitx5-chinese-addons安装完成,重启电脑,然后按Ctrl + 空格即可切换到中文输入法。
之前做了个教程是在Windows上使用Cygwin来编译Redis,不过今天发现了个更好的方案,在Windows上,使用msys2能够编译最新版的Redis。今天我就来分享一下编译的过程。msys2和cygwin一样,都是在Windows下可以执行Linux命令并编译Linux软件的环境1,下载安装Msys2并配置镜像源去官网下载并安装Msys2,安装过程很简单,这里就不再赘述安装过程了。默认安装在C:\msys64目录下,安装完成后,打开安装目录下的msys2.exe即可打开msys2控制台,这个文件就是其主程序,可以创建一个快捷方式到桌面。msys2使用的是pacman进行包管理,我们先熟悉一下常用的pacman命令:# 安装软件包 pacman -S 软件包1 [软件包2...] # 卸载软件包 pacman -R 软件包 # 卸载软件包及其依赖包 pacman -Rs 软件包 # 更新软件索引并更新系统 pacman -Syu首先我们要更换msys2的软件源,使其下载速度更快,我们更换清华的源,参照清华源官方的换源教程即可:换源教程按照这个教程换完源之后,打开msys2的控制台,执行命令更新所有包:pacman -Syu按照控制台中的英文提示完成更新系统即可。第一次更新后可能会提示关闭msys2重启,确认后控制台会被关闭,但这时没有完全更新完毕,需要重新打开控制台再执行更新命令。2,安装编译Redis必要的软件包打开msys2控制台,执行以下命令安装编译所需的gcc和make包:pacman -S msys/gcc msys/make安装完成后,我们还需要修改msys2中的一个库文件,否则编译Redis的时候会报错找不到符号Dl_info。用文本编辑器打开msys2安装目录下的usr/include/dlfcn.h这个文件,找到49行这个位置,如下图,将49行#if __GNU_VISIBLE和61行的#endif这两行内容删掉,保存。(建议修改之前先备份这个文件)3,编译Redis去Redis官网下载源代码并解压。(备用下载,提取码:2333)在msys2控制台中,使用cd命令进入到解压后Redis源码所在目录下:cd "源码目录"注意,命令中路径都要用英文双引号包围,下面的命令中路径也是一样。然后开始编译:make最后出现Hint: It's a good idea to run 'make test' ;)说明编译成功。再通过以下命令把二进制可执行文件提取出来:make PREFIX="要提取到的路径" install例如我:make PREFIX="C:\Users\swsk33\Downloads\redis-6.2.6-已编译" install然后在指定目录中就出现了bin文件夹,这个文件夹中就是编译好了的Redis的Windows二进制文件,可以直接执行。不过直接打开会提示找不到dll的错误:这时我们只需要在msys2的安装目录中的usr/bin目录下,找到msys-2.0.dll这个文件,复制一个到我们Redis已编译的二进制exe文件同目录下,即可直接运行了:现在,将这个Redis的exe文件所在目录加入Path环境变量,即可使用命令行调用Redis的命令了!4,Windows添加Redis服务端开机自启如果你仅仅是想连接已有的远程Redis服务端,就可以不用看这一部分!为了方便起见,我们可能需要编译完成之后,在本地运行Redis服务端以进行我们平时开发测试。因此我们可以让Redis开机自启动。首先我们要知道两个可执行文件作用:redis-cli.exe Redis客户端,用于连接Redis服务器redis-server.exe Redis服务端,用于在本机运行Redis服务器由于Redis是msys2编译的,因此使用绝对路径启动命令可能出现不兼容情况导致启动失败。所以说我们先写一个批处理脚本用于以相对路径启动Redis,放在Redis所在目录下(redis-server.exe同级目录下),再把这个脚本加入到注册表的开机自启动项中即可。在Redis所在目录下新建一个文本文件并修改扩展名为bat:用文本编辑器打开,把下面命令粘贴进去保存:@echo off cd /d %~dp0 redis-server redis.conf上述第二行cd /d %~dp0表示先切换当前路径至批处理文件自身所在路径下,%~dp0在批处理文件中表示批处理文件自身所在目录。由于这个脚本放在Redis所在目录下,因此这条语句就达到了先切换当前路径到Redis目录下的目的,就可以后续以相对路径启动Redis了!上面第三行就是Redis的启动命令,redis.conf是我的redis配置文件,也位于Redis所在目录下。如果对Redis启动命令和配置文件不明白可以看看:传送门编写完批处理后,打开注册表,找到HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run这一项:这一项中的所有值都是开机自启动项。在右边右键-新建-字符串值,名称随意,值为"上述启动Redis的批处理文件位置":注意注册表值中,路径必须是启动批处理文件的绝对路径并用英文双引号包围!确定,然后就成功地添加了开机自启动项!若后续不需要Redis再开机自启动,就把上述新建的注册表值删除即可!
今天在做Vue开发的时候遇到了个很奇怪的现象:改变了Vuex中的state的数据,但是页面没有做出改变。首先放出代码:Vuex:import { createStore } from 'vuex'; /** * 用户构造函数 * @param {*} name 用户名 */ function User(name) { this.name = name; /** * 修改用户名 * @param {String} changedName 修改后用户名 */ this.changeName = (changedName) => { this.name = changedName; } } export default createStore({ state: { user: new User('swsk33') }, mutations: { change(state) { state.user.changeName('swsk'); console.log(state.user.name); } }, actions: { change(context) { context.commit('change'); } }, });Vue组件:<template> <div id="app"> <div>{{ user.name }}</div> <div @click="change">更改</div> </div> </template> <script> import { mapState, mapActions } from 'vuex'; export default { computed: { ...mapState(['user']), }, methods: { ...mapActions(['change']), }, }; </script>可见,在Vuex中,方便起见我定义了个构造函数,用于创建用户对象。可以看到,用户对象中有一个changeName函数可以更改自己的用户名,这个函数相当于可以更改自己的值。然后在state中初始化一个用户对象,并定义好mutations,actions函数以测试更改,注意mutations中方法是使用用户对象自己的方法修改用户名,而不是直接赋值。最后,vue组件中,映射Vuex中的user和change函数进行测试。理论上,我点击更改时,显示的用户名会变,然而点击后并没有变。点击后网页:网页上内容并没有变。但是在控制台中输出这个state中的user,实质上已经改变了:我们知道,state中的数据也是响应式的,而更改其数据的唯一方法就是mutations。但是可见,上述修改数据的形式并不同于平时,没有在mutations进行直接赋值,而是使用对象自己的方法修改自己。这就是问题所在:mutations中的函数没有进行一个直接的更改对象的操作,而是间接地调用对象自己的方法修改对象自己。这样,数据确实修改了,但是可以理解为:并非是mutations进行修改的。这种情况下,mutations没有直接修改数据,就导致Vue中无法侦测到数据变化做出响应。解决办法很简单,不要在构造函数中定义修改对象自己属性的方法,而是使用mutations修改即可。我们修改上述Vuex如下:import { createStore } from 'vuex'; /** * 用户构造函数 * @param {*} name 用户名 */ function User(name) { this.name = name; } export default createStore({ state: { user: new User('swsk33') }, mutations: { changeName(state) { // 进行直接修改操作 state.user.name = 'swsk'; } }, actions: { change(context) { context.commit('changeName'); } }, });所以,使用Vuex时,一定要牢记官方文档的这一句话:更改Vuex的store中的状态的唯一方法是提交mutation。任何修改数据的操作应当放在mutations中,而非别的函数里面。
在浏览网页的过程中,模态框其实并不陌生。不过在vue开发过程中,应当如何封装一个vue组件为可以复用的模态框呢?1,思路首先我们不难发现,模态框有以下特点:有一个半透明蒙层,防止用户没有做完模态框的命令就操作其它内容显示在最顶层,这说明很多模态框是直接放在body元素中的我先开始就想,能不能先在vue文件中把模态框样式定义好,然后利用props或者slot传值、利用refs调用其方法呢?后来发现这样不仅不方便(每次都要注册组件)、还无法把模态框直接放在body里面。参考了官方文档,我发现其实再写一个配套的js文件,在js文件中定义显示模态框的函数,借助这个js在body元素下创建一个元素,并将vue组件挂载上去即可。下面我们就一一来实现。2,编写vue文件,完成模态框基本样式首先需要完成一些对话框的基本样式,我在components目录下创建vue文件,内容如下:<template> <div class="mydialog"> <div class="frame"> <div class="content"> <img :src="image" /> <div class="text">{{ text }}</div> </div> <div class="buttons"> <div class="ok" @click="clickOk">确定</div> <div class="cancel" @click="clickCancel">取消</div> </div> </div> </div> </template> <script> export default { name: 'mydialog', data() { return { /** * 显示文字 */ text: '', /** * 显示图片 */ image: '', /** * 模态框挂载的DOM元素,用于后面销毁模态框 */ mountDom: '', }; }, methods: { /** * 自定义确定事件 */ ok() {}, /** * 自定义取消事件 */ cancel() {}, clickOk() { // 先执行自定义确定方法 this.ok(); // js传入该模态框实例挂载的元素,关闭时将其销毁 this.mountDom.remove(); }, clickCancel() { // 先执行自定义取消方法 this.cancel(); // js传入该模态框实例挂载的元素,关闭时将其销毁 this.mountDom.remove(); }, }, }; </script> <style lang="scss" scoped> .mydialog { position: absolute; width: 100vw; height: 100vh; left: 0; top: 0; display: flex; justify-content: center; align-items: center; background-color: rgba(255, 255, 255, 0.7); .frame { width: 560px; height: 250px; background-color: rgb(177, 255, 229); box-shadow: 1px 1px 7px 2px rgb(90, 203, 255); border-radius: 5px; display: flex; flex-direction: column; justify-content: space-evenly; align-items: center; .content { display: flex; width: 85%; justify-content: flex-start; align-items: center; img { height: 64px; } .text { font-size: 24px; margin-left: 24px; } } .buttons { display: flex; width: 80%; justify-content: space-around; align-items: center; font-size: 26px; .ok, .cancel { width: 75px; height: 32px; text-align: center; line-height: 32px; border-radius: 6px; user-select: none; cursor: pointer; } .ok { &:hover { color: white; background-color: blue; } } .cancel { &:hover { color: white; background-color: rgb(255, 0, 98); } } } } } </style>这里重点是在data()中我定义了一些变量,用于我们可以自定义模态框内容。问题是,模态框这么销毁自己呢?就算是使用v-if关闭了自己,但是前面提到了,这个模态框是要挂载至一个元素中的,这个挂在元素无法被消除怎么做呢?所以可以看到上面还有一个mountDom变量,让js创建挂载元素之后将其传入,这样vue就可以直接销毁这个挂载元素了!除此之外,还预留了ok和cancel两个函数用于按下“确定”和“取消”之后的自定义事件,传入自定义的函数赋值给ok和cancel即可。3,编写js文件,封装方法以供调用,给模态框组件传参在同级目录下创建js文件,内容如下:import { createApp } from 'vue'; import MyDialog from './MyDialog.vue'; /** * 显示自定义对话框 * @param {String} text 对话框提示内容 * @param {NodeRequire} image 对话框图标(需要require图片路径) * @param {Function} ok 自定义确定事件 * @param {Function} cancel 自定义取消事件 */ export function showDialog(text, image, ok, cancel) { // 创建一个div元素放到body下面,供模态框vue组件挂载 let mountDom = document.createElement('div'); mountDom.style.position = 'absolute'; mountDom.style.left = 0; mountDom.style.top = 0; document.body.appendChild(mountDom); // 挂载组件以显示,mount函数返回vue组件实例 let dialog = createApp(MyDialog).mount(mountDom); // 设定vue实例中的变量,将一些数据传入vue dialog.text = text; dialog.image = image; dialog.ok = ok; dialog.cancel = cancel; // 传入挂载的dom是为了vue组件关闭时可以直接销毁这个挂载的dom达到销毁模态框的目的 dialog.mountDom = mountDom; }可见这里就很简单了。定义一个函数,先创建一个div元素加到body中用于挂载对话框组件,然后利用createApp函数挂载这个对话框组件至创建的div元素之下即可。看到这,大家也知道了怎么在js中,向vue实例传递值或者调用函数了。在官方文档中,对于createApp函数,有这样一句话:与大多数应用方法不同的是,mount不返回应用本身。相反,它返回的是根组件实例。我们知道,每一个vue组件可以被作为一个实例使用,那么mount函数,不仅仅是挂载了组件至指定的元素下,还会返回其实例,我们也可以调用这个实例中的变量、函数,实现传递参数的目的。mount函数中,可以是dom元素实例,也可以是字符串形式的选择器(例如'#app',挂载到id为app的元素下)。4,测试现在我们在根组件里面写一个按钮并调用试试:<template> <div class="app"> <button @click="testShowDialog">显示模态框</button> </div> </template> <script> // 先引入js中显示对话框函数 import { showDialog } from './components/mydialog.js'; export default { name: 'app', methods: { testShowDialog() { showDialog( '提示:少时诵诗书所所所所所所所所所所所所所所所所所所所所所所所所所所所所所所所', require('./assets/1.png'), () => { console.log('点击了确定'); }, () => { console.log('点击了取消'); } ); }, }, }; </script>效果:我们来看看html中元素:再结合一下上面的js文件看看,相信能够理解这个思路。5,总结可见封装一个可复用的vue组件并不难,先写好样式,再写一个配套的js文件给其传参、用于调用即可。其实封装其它类型可复用组件,也是一样的思路。示例代码仓库地址
最近决定使用Markdown做笔记,于是使用了Typora作为工具。不过图片上传就是个问题,使用公共图床不能保证文件稳定性,于是决定使用阿里云OSS作为图床。1,在阿里云购买OSS服务并创建容器阿里云的OSS服务虽然可以免费开通,但是使用是要计费的。因此最好是先购买包年或者包月的套餐。登录阿里云,在产品中选择对象储存OSS:选择套餐,个人使用一般最小的40GB即可,注意地区选择,一般选择中国大陆:购买完成,进入OSS的控制台,在这里可以看已经购买的资源包:在Bucket列表中先创建一个Bucket(可以理解为一个容器):注意创建的时候地域一定要和你购买的地域一样,否则会额外计费(如果是中国大陆通用那可以随意选择中国内地区域的),储存类型也要一致,一般就是标准储存:权限选择公共读:以及只作为图床的话,建议关闭版本控制。然后回到OSS控制台概览,在右下角创建Access Key:你会获得一个Access Key Id和一个私钥(Secret Key),最好是下载下来保存好。2,在Typora中配置在软件偏好设置-图像这里,选择PicGo-Core(如果已经安装了PicGo客户端就选app的,没有安装建议使用命令行)然后点击下载按钮下载命令行PicGo:等待下载完成,点击打开配置文件按钮,打开里面内容默认如图:在配置文件里面,把picBad这一项配置为如下内容:"current": "aliyun", "aliyun": { "accessKeyId": "你的AccessKeyId", "accessKeySecret": "你的私钥", "area": "你的OSS地域代号", "bucket": "你创建的bucket名", "customUrl": "(非必须)自定义域名", "path": "(非必须)自定义储存路径,指定上传到bucket的哪个目录" }OSS地域代号如下:地区代号华东1(杭州)oss-cn-hangzhou华东2(上海)oss-cn-shanghai华北1(青岛)oss-cn-qingdao华北2(北京)oss-cn-beijing华北 3(张家口)oss-cn-zhangjiakou华北5(呼和浩特)oss-cn-huhehaote华北6(乌兰察布)oss-cn-wulanchabu华南1(深圳)oss-cn-shenzhen华南2(河源)oss-cn-heyuan华南3(广州)oss-cn-guangzhou西南1(成都)oss-cn-chengdu中国(香港)oss-cn-hongkong美国西部1(硅谷)oss-us-west-1美国东部1(弗吉尼亚)oss-us-east-1亚太东南1(新加坡)oss-ap-southeast-1亚太东南2(悉尼)oss-ap-southeast-2亚太东南3(吉隆坡)oss-ap-southeast-3亚太东南5(雅加达)oss-ap-southeast-5亚太东北1(日本)oss-ap-northeast-1亚太南部1(孟买)oss-ap-south-1欧洲中部1(法兰克福)oss-eu-central-1英国(伦敦)oss-eu-west-1中东东部1(迪拜)oss-me-east-1可以在官方帮助看地区代号。最终配置如图:配置完成,点击验证上传,提示成功即可:记得图片插入选项设置为上传:PicGo配置文档
有时候需要使用C#发送网络请求,其中上传文件其实也是使用POST请求。方式也有很多,除了自己组装数据之外,还可以使用HttpClient发送。上传文件使用HttpClient类其实可以很简单的实现。1,实例化MultipartFormDataContent类组装表单在C#中,MultipartFormDataContent类是一个专门用于存放multipart表单数据的类,通过其Add方法可以将表单数据加进去。假设这里有个上传文件的接口如下:地址:/upload方法:POST参数:字段类型描述nameString名字fileFile文件也就是说,表单项有一个字符串类型字段,和一个文件,这个时候在C#组装表单如下:// 读取文件 Stream fileStream = new FileStream(@"C:\Users\swsk33\Pictures\wallpaper-2x\gz-3.png", FileMode.Open, FileAccess.Read); // 实例化multipart表单模型 var formData = new MultipartFormDataContent(); // 设定文本类型表单项,使用StringContent存放字符串 formData.Add(new StringContent("测试图片"), "name"); // 设定文件类型表单项,使用StreamContent存放文件流 formData.Add(new StreamContent(fileStream), "file", "a.png");可见MultipartFormDataContent的方法Add可以把我们的表单项加进去,上面第一个Add中第一个参数是StringContent类型,用于存放字符串,第二个参数为接口要求的字段。第二个Add参数中有三个参数,第一个为StreamContent,存放待上传的文件流,可以看到可以先用FileStream读取文件为文件流再存入,第二个参数为接口要求的字段,第三个参数为文件名,可以自己起,但是扩展名最好是和原来保持一致。添加文件除了直接放入文件流之外,还可以先读取为字节数据存入,效果一样:Stream fileStream = new FileStream(@"C:\Users\swsk33\Pictures\wallpaper-2x\gz-3.png", FileMode.Open, FileAccess.Read); byte[] data = new byte[fileStream.Length]; fileStream.Read(data, 0, data.Length); fileStream.Close(); formData.Add(new ByteArrayContent(data), key, "gz-3.png");这样,表单就组装完毕了!2,实例化HttpClient发送请求这个就很简单了:// 实例化HttpClient HttpClient client = new HttpClient(); // 发送请求 HttpResponseMessage result = client.PostAsync("http://127.0.0.1:8806/upload", formData).Result; // 接受结果 string responseResult = result.Content.ReadAsStringAsync().Result; // 打印结果 Console.WriteLine(responseResult); client.Dispose(); fileStream.Close();HttpClient有一个PostAsync方法表示发送POST请求,第一个参数为请求地址,第二个为刚刚组装的表单对象,这是一个异步方法,不过向上述一样调用其返回值的Result可以等待其完成,这样会返回一个HttpResponseMessage对象,为响应内容,再调用其Content的ReadAsStringAsync方法可以读取响应内容为字符串,这也是异步方法,不过还是向上面一样调用其Result可以等待其完成。这样就完成了上传文件请求。3,自定义请求参数上面的操作其实是一个“简写”,如果需要自定义请求头等等,需要先定义HttpRequestMessage对象(表示一个Http请求)设定其中请求头、请求内容等等,再使用HttpClient发送即可。// 实例化HttpRequestMessage对象 HttpRequestMessage requestMessage = new HttpRequestMessage(); // 设定请求类型为POST requestMessage.Method = HttpMethod.Post; // 设定请求地址 requestMessage.RequestUri = new Uri("http://www.example.com/upload"); // 实例化multipart表单模型 var formData = new MultipartFormDataContent(); // ...省略加入内容至表单的过程... // 设置请求体为上述multipart表单 requestMessage.Content = formData; // 设定Content-Type request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); // 加入其他自定义请求头,这里以UA为例,还可以设定其它自定义请求头(注意Content-Type在这里设置是无效的,必须按照上面一行设置) requestMessage.Headers.TryAddWithoutValidation("User-Agent", "xxx..."); // 实例化HttpClient HttpClient client = new HttpClient(); // 发送请求 HttpResponseMessage result = client.SendAsync(requestMessage).Result; // 接受结果 string responseResult = result.Content.ReadAsStringAsync().Result; // 打印结果 Console.WriteLine(responseResult);自定义HttpRequestMessage实例之后,使用HttpClient的SendAsync方法即可。
gitlab是一个基于Git实现的在线代码仓库托管软件,可以使用gitlab自己搭建一个类似于Github一样的系统,方便一个企业、组织或者学校内部进行开发学习等等。今天我来分享一下,gitlab在服务器上的安装配置,以Debian为例。1,安装gitlabgitlab有ce和ee两个版本,分别是免费的社区版和有付费功能的企业版,平时小型组织使用社区版即可,今天也以安装社区版为例。首先安装依赖:sudo apt install curl openssh-server ca-certificates perl postfix安装过程中会显示postfix的配置界面:这里选择Internet Site,确定,下一个填写mail name,自行起一个就行:然后添加gitlab的仓库:curl -sS https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash然后安装gitlab-ce:sudo apt install gitlab-ce其安装包非常大,可能要很长时间。等待安装完成即可!2,配置gitlab找到/etc/gitlab/gitlab.rb,这个就是gitlab的配置文件,使用文本编辑器打开,我们只需修改一些常用配置即可。里面很多配置都是默认被开头的#注释掉了,记得先去掉开头的#(去掉注释)再配置值。(1) 访问地址配置在配置文件中找到external_url这一项,将其改为你的服务器访问域名或者是ip地址,如果不想使用标准端口还可以设置为别的端口(例如xx.com:2122),内网服务器一般设置为内网地址:如果你的地址是https的,还需要配置ssl证书,准备好一个有效的crt证书,然后修改以下配置项:nginx['enable'] = true nginx['redirect_http_to_https'] = true nginx['redirect_http_to_https_port'] = 80 nginx['ssl_certificate'] = "crt证书位置" nginx['ssl_certificate_key'] = "证书密钥位置"如果说你的证书是用Let's Encrypt生成的pem格式,可以用下列命令转换为crt和key:# 将证书pem文件转换为crt文件 openssl x509 -in "证书pem文件路径" -out "输出crt文件路径" # 将证书私钥pem文件转换为key文件 openssl rsa -in "私钥pem文件路径" -out "输出key文件路径"Let's Encrypt生成的证书在/etc/letsencrypt/live/你的域名目录下:(2) 权限配置默认gitlab的普通用户也可以创建组,不过一般这个需要关掉,找到gitlab_rails['gitlab_default_can_create_group']将其配置为false:(3) 邮箱配置首先要准备一个邮箱,163、qq的都行,并开启smtp服务。然后找到以下配置值并配置为如下:gitlab_rails['smtp_enable'] = true gitlab_rails['smtp_address'] = "smtp服务器地址" gitlab_rails['smtp_port'] = 465 gitlab_rails['smtp_user_name'] = "你的邮箱" gitlab_rails['smtp_password'] = "授权码" gitlab_rails['smtp_domain'] = "smtp服务器地址" gitlab_rails['smtp_authentication'] = "login" gitlab_rails['smtp_enable_starttls_auto'] = true gitlab_rails['smtp_tls'] = true gitlab_rails['smtp_pool'] = false gitlab_rails['gitlab_email_from'] = '你的邮箱' gitlab_rails['gitlab_email_display_name'] = '发件人显示名'需要注意的是,gitlab_rails['gitlab_email_from']这一项也一定要配置为你的邮箱地址,然后现在基本上都使用465端口发送邮件,因此gitlab_rails['smtp_tls']一定要是true。(4) 代码存放位置配置找到git_data_dirs,把其中的path改为自定义值:没有特殊需要这个可以不配置。(5) 备份设置找到以下配置并配置如下:gitlab_rails['manage_backup_path'] = true gitlab_rails['backup_path'] = "备份位置" gitlab_rails['backup_keep_time'] = 604800上述gitlab_rails['backup_keep_time']表示备份保留时长。备份设置没有特殊需要也可以不用修改。最后执行命令重载配置:sudo gitlab-ctl reconfigure然后,gitlab服务端就启动了!gitlab服务端常用命令:# 启动服务端 sudo gitlab-ctl start # 停止服务端 sudo gitlab-ctl stop # 重启服务端 sudo gitlab-ctl restart # 重载配置 sudo gitlab-ctl reconfigure # 查看状态 sudo gitlab-ctl status3,访问并登录管理员账户启动完成之后,就可以访问服务器地址,登录了。管理员账户名默认为root,密码可以在/etc/gitlab/initial_root_password文件中找到:登录后记得修改管理员密码。4,备份与恢复我们可以手动创建备份:sudo gitlab-rake gitlab:backup:create由上述配置可知,备份文件默认在/var/opt/gitlab/backups目录下。备份的文件为一个tar文件。恢复则使用下列命令:sudo gitlab-rake gitlab:backup:restore BACKUP=备份编号可以在备份目录查看自己的备份文件,如果备份文件名为:1632904480_2021_09_29_14.3.0_gitlab_backup.tar,那么备份编号就是1632904480_2021_09_29_14.3.0。注意,备份和恢复时,必须保证gitlab是正在运行状态,并且备份文件必须和版本匹配,如果低版本备份文件恢复到高版本是不行的。并且/etc/gitlab目录下的gitlab.rb和gitlab-secrets.json这两个文件是不会被备份的,需要手动复制出来,最后放回去。5,更新先开始安装时已经在系统加入了gitlab软件源了,后续gitlab如果需要更新,执行apt update和apt full-upgrade即可更新。需要注意的是,更新时必须保证gitlab服务器是启动状态,否则可能失败。如果仍然更新失败,可能是版本跨太多了,可以先使用apt list -a gitlab命令查看gitlab的版本,然后一级一级地向上更新。安装指定版本软件:sudo apt install 软件名=版本号6,总结至此基本安装配置就完成了,下面给出一些参考地址:gitlab官方安装说明gitlab官方配置文档完全卸载gitlab参考
AOP,即面向切面编程,其核心思想就是把业务分为核心业务和非核心业务两大部分。例如一个论坛系统,用户登录、发帖等等这是核心功能,而日志统计等等这些就是非核心功能。在Spring Boot AOP中,非核心业务功能被定义为切面,核心和非核心功能都开发完成之后,再将两者编织在一起,这就是AOP。AOP的目的就是将那些与业务无关,却需要被业务所用的逻辑单独封装,以减少重复代码,减低模块之间耦合度,利于未来系统拓展和维护。今天,我将做一个简单的打印用户信息的程序,即后端接受POST请求中的User对象将其打印这样一个逻辑,在这个上面实现AOP。首先放上用户打印服务逻辑的方法代码:Service层:package com.example.springbootaop.service.impl; import com.example.springbootaop.model.User; import com.example.springbootaop.service.UserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Component public class UserServiceImpl implements UserService { /** * 使用Logger */ private Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public void printUserInfo(User user) { logger.info("用户id:" + user.getId()); logger.info("用户名:" + user.getUsername()); logger.info("用户昵称:" + user.getNickname()); } }Control层:package com.example.springbootaop.api; import com.example.springbootaop.model.User; import com.example.springbootaop.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController public class UserAPI { @Autowired private UserService userService; @PostMapping("/user") public String printUser(@RequestBody User user) { userService.printUserInfo(user); return "已完成打印!"; } }User是一个简单的封装类,这里就不展示了,文章末尾会给出整个示例程序代码地址。1,AOP到底有什么不同这里只是实现一个简单的逻辑,打印用户信息,这就是我们今天的核心功能。如果这个时候我们要给它加上非核心功能:在打印之前和打印之后分别执行一个方法,如果你不知道AOP,你可能会把Control层的方法改成如下形式:@PostMapping("/user") public String printUser(@RequestBody User user) { // 执行核心业务之前 doBefore(); // 执行核心业务 userService.printUserInfo(user); // 执行核心业务之后 doAfter(); ... return "已完成打印!"; }如果说方法多了,业务多了,非核心业务的逻辑一变,所有Controller的全部方法都要改动,非常麻烦,且代码冗余,耦合度高。这时,就需要AOP来解决这个问题。AOP只需要我们单独定义一个切面,在里面写好非核心业务的逻辑,即可将其织入核心功能中去,无需我们再改动Service层或者Control层。2,AOP中的编程术语和常用注解在学习AOP之前,我们还是需要了解一下常用术语:切面:非核心业务功能就被定义为切面。比如一个系统的日志功能,它贯穿整个核心业务的逻辑,因此叫做切面切入点:在哪些类的哪些方法上面切入通知:在方法执行前/后或者执行前后做什么前置通知:在被代理方法之前执行后置通知:在被代理方法之后执行返回通知:被代理方法正常返回之后执行异常通知:被代理方法抛出异常时执行环绕通知:是AOP中强大、灵活的通知,集成前置和后置通知切面:在什么时机、什么地方做什么(切入点+通知)织入:把切面加入对象,是生成代理对象并将切面放入到流程中的过程(简而言之,就是把切面逻辑加入到核心业务逻辑的过程)在Spring Boot中,我们使用@AspectJ注解开发AOP,首先需要在pom.xml中引入如下依赖:<!-- Spring Boot AOP --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>然后就可以进行AOP开发了!这里先给出常用注解,大家联系着下面的例子看就可以了:@Pointcut 定义切点@Before 前置通知@After 后置通知@AfterReturning 返回通知@AfterThrowing 异常通知@Around 环绕通知3,定义切面新建aop包,在里面新建类作为我们的切面类,先放出切面类代码:package com.example.springbootaop.aop; import org.aspectj.lang.annotation.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Aspect @Component public class LogAspect { /** * 日志打印 */ private Logger logger = LoggerFactory.getLogger(this.getClass()); /** * 使用Pointcut给这个方法定义切点,即UserService中全部方法均为切点。<br> * 这里在这个log方法上面定义切点,然后就只需在下面的Before、After等等注解中填写这个切点方法"log()"即可设置好各个通知的切入位置。 * 其中: * <ul> * <li>execution:代表方法被执行时触发</li> * <li>*:代表任意返回值的方法</li> * <li>com.example.springbootaop.service.impl.UserServiceImpl:这个类的全限定名</li> * <li>(..):表示任意的参数</li> * </ul> */ @Pointcut("execution(* com.example.springbootaop.service.impl.UserServiceImpl.*(..))") public void log() { } /** * 前置通知:在被代理方法之前调用 */ @Before("log()") public void doBefore() { logger.warn("调用方法之前:"); logger.warn("接收到请求!"); } /** * 后置通知:在被代理方法之后调用 */ @After("log()") public void doAfter() { logger.warn("调用方法之后:"); logger.warn("打印请求内容完成!"); } /** * 返回通知:被代理方法正常返回之后调用 */ @AfterReturning("log()") public void doReturning() { logger.warn("方法正常返回之后:"); logger.warn("完成返回内容!"); } /** * 异常通知:被代理方法抛出异常时调用 */ @AfterThrowing("log()") public void doThrowing() { logger.error("方法抛出异常!"); } }切面类需要打上@Aspect注解表示这是一个切面类,然后不要忘了打上@Component注解。我们逐步来看。首先是定义切点,只需定义一个空方法,在上面使用@Pointcut注解即可,注解里面内容含义如下:execution 代表方法被执行时触发* 代表任意返回值的方法com.example.springbootaop.service.impl.UserServiceImpl 被织入类的全限定名(..) 表示任意的参数定义完切点之后,就可以定义各个通知的方法逻辑了,这些就是我们的切面逻辑,也就是非核心业务的逻辑。上面在doBefore方法上面,我们使用了@Before注解,这样就标明了doBefore方法是前置通知逻辑,会在被织入方法之前执行。我们把log方法定义为切入点,然后下面各个通知注解中,填写这个切入点方法名称即可。我们也并不需要定义所有的通知,只需定义需要的即可。其实,如果不定义上面的切入点方法log和@Pointcut,你仍然可以把execution表达式直接写在各个通知的注解里面,例如:/** * 前置通知:在被代理方法之前调用 */ @Before("execution(* com.example.springbootaop.service.impl.UserServiceImpl.*(..))") public void doBefore(JoinPoint joinPoint) { logger.warn("调用方法之前:"); logger.warn("接收到请求!"); }但是大多数情况并不推荐这样,这种写法较为复杂。我们发送一个请求测试一下:通过这个,我们也可以发现各个通知的执行顺序:Before -> AfterReturning -> After4,环绕通知环绕通知是AOP中最强大的通知,可以同时实现前置和后置通知,不过它的可控性没那么强,如果不用大量改变业务逻辑,一般不需要用到它。我们在上述切面加入下列环绕通知方法:/** * 环绕通知 */ @Around("log()") public void around(ProceedingJoinPoint joinPoint) { logger.warn("执行环绕通知之前:"); try { joinPoint.proceed(); } catch (Throwable e) { e.printStackTrace(); } logger.warn("执行环绕通知之后"); }通知方法中有一个ProceedingJoinPoint类型参数,通过其proceed方法来调用原方法。需要注意的是环绕通知是会覆盖原方法逻辑的,如果上面代码不执行joinPoint.proceed();这一句,就不会执行原被织入方法。因此环绕通知一定要调用参数的proceed方法,这是通过反射实现对被织入方法调用。再次测试如下:5,通知方法传参上面每个通知方法是没有参数的。其实,通知方法是可以接受被织入方法的参数的。我们上述被织入方法参数就是一个User对象,因此通知方法也可以加上这个参数接受。我们改变前置通知方法如下:/** * 前置通知:在被代理方法之前调用 */ @Before("log() && args(user)") public void doBefore(User user) { logger.warn("调用方法之前:"); logger.warn("接收到请求!"); logger.warn("得到用户id:" + user.getId()); }测试结果:可见在注解后面加一个args选项,里面写参数名即可。需要注意的是,通知方法的参数必须和被织入方法参数一一对应例如:/** * 被织入方法 * / public void print(User user, int num) { ... } /** * 通知 * / @Before("log() && args(user, num)") public void doBefore(User user, int num) { ... }6,总结AOP其实使用起来是个很方便的东西,大大降低了相关功能之间的耦合度,使得整个系统井井有条。定义切面,然后定义切点,再实现切面逻辑(各个通知方法),就完成了一个简单的切面。示例程序仓库地址
Minecraft(我的世界)是一款非常好玩的游戏,其多人联机玩法,更是增加了许多乐趣。不过我们应当如何搭建服务器,才能和我们的小伙伴们一起玩呢?今天我就来介绍一下,如何搭建Minecraft原版或者模组服务器。1,准备首先,我们需要购买一台Linux服务器,建议选择国内节点,延迟会低很多。然后下载安装远程服务器管理软件FinalShell。2,服务器安装Java运行环境说在前面的是,不同版本的Minecraft服务端需要使用的Java版本也可能有所不同。不过大多数使用Java 8即可。选择正确的Java版本或者jvm可以减少内存占用,提高CPU效率。当然这里做一个推荐。原版服务器:1.11.2版本及其以下:使用Oracle JDK8或者Alibaba Dragonwell 81.12版本及其以上:使用Alibaba Dragonwell 11或者Alibaba Dragonwell 17Forge服务器:1.12.2版本及其以下:使用Oracle JDK8或者Alibaba Dragonwell 81.13版本及其以上:使用Alibaba Dragonwell 11上面建议已经提供了各个Java下载地址,建议下载压缩包形式,下载好Java之后,将其上传至服务器解压或者安装,并进行JDK环境变量配置,环境变量配置可以看这个博客:链接配置完成,断开重连服务器,输入命令:java -version有版本信息输出则成功:在此Java配置就完成了!如果想进一步了解不同的JDK对Minecraft服务端性能影响可以参考:文章3,开服到这里就可以启动Minecraft服务器了!这里我将分为原版和Forge模组服务器分别讲解。(1) 原版去MCVersions下载你想要的版本的服务端核心,在网站左侧稳定版找到你想要的版本点击Download(我下载1.12.2版本):进入下载页面,点击下载Server jar下载服务端核心:然后将下载的jar文件上传至你的服务器的某个文件夹下,然后使用cd命令进入该文件夹(一定要使用cd命令进入!)我下载文件为server.jar,上传并cd进所在文件夹之后执行命令启动服务端:java -jar server.jar nogui也可以加上jvm参数限制运行内存:# 限制最小内存为256MB,最大内存为1024MB java -Xms256M -Xmx1024M -jar server.jar nogui第一次启动会失败,因为需要你同意一下EULA协议,刷新目录可见当前目录生成了个eula.txt:修改里面的false为true:然后重新执行上述命令,即可启动服务端,稍等片刻,最后显示 done! 字样说明启动成功:不过因为是第一次启动,我们仍然需要修改一些配置才能玩,所以先输入stop停止服务端。刷新目录,发现已经生成了很多文件,找到server.properties文件,这个是服务端配置文件,我们需要进行一些修改,里面内容如下:上面的online-mode一定要改为false否则非正版玩家无法进入。其余根据实际情况配置,端口的话即可也要配置一下服务器防火墙开放对应端口。再次执行启动命令:java -jar server.jar nogui等待启动完成即可!然后打开游戏->多人游戏->添加服务器(或者直接连接),地址填:你的服务器外网地址:服务器端口然后就可以进入游戏了!(2) Forge模组服务器去Forge官网下载你想要的版本的Forge,下载installer版(还是1.12.2为例):得到一个jar文件,将其上传至服务器某个目录下,并使用cd命令进入该目录,执行安装命令:java -jar "forge-installer文件名.jar" --installServer例如我下载的文件文件名为forge-1.12.2-14.23.5.2855-installer.jar,那么我就使用cd命令进入其目录,执行:java -jar "forge-1.12.2-14.23.5.2855-installer.jar" --installServer然后它就开始下载相应的依赖库,需要等一会,因为是从外网下载因此可能很慢或者失败,可以多试几次。如果一直下载不成功且你有代理的话,可以在执行Forge Installer时通过jvm参数指定代理:# 例如:java -Dhttp.proxyHost="127.0.0.1" -Dhttp.proxyPort="1080" -Dhttps.proxyHost="127.0.0.1" -Dhttps.proxyPort="1080" -jar "forge-1.12.2-14.23.5.2855-installer.jar" --installServer java -Dhttp.proxyHost="http代理地址" -Dhttp.proxyPort="http代理端口" -Dhttps.proxyHost="https代理地址" -Dhttps.proxyPort="https代理端口" -jar "forge installer文件" --installServer最后出现successfully字样则成功:刷新目录,我们发现会多出一个文件名形如forge-x.x.xx-xxx.jar的文件,这个就是主要的forge服务端文件,待会需要执行它启动。(如果是1.7.10版本或者低版本的forge那么这个主服务端文件名应该是forge-x.x.xx-xxx-universal.jar的形式):我们可以先删除installer的jar文件和log,因为用不着了:然后使用命令启动forge服务端,例如我这个1.12.2版本的:java -jar "forge-1.12.2-14.23.5.2855.jar" nogui第一次启动也会失败,这个和上面原版服务器一样,去同意一下eula就行。可见启动命令也差不多的,都是执行相应的jar文件,只不过这里执行的是forge服务端文件,同样可以向上面一样加上jvm参数限制内存。修改了eula之后再启动,出现done即成功:还是和上面原版服务器一样,stop停止,找到配置文件server.properties,修改online-mode为false,其余按需修改。这个时候我们发现已经生成了mods文件夹,我们就可以把模组文件上传至这个文件夹。需要注意的是,一些模组例如工业2中一些东西需要自然生成,但是刚刚已经生成了世界了,这个时候我们可以先删除世界数据文件夹world(平时也可以用这个方法重新生成世界),待会启动服务端时重新生成:这个时候配置完配置文件和mod,以及删掉世界后就可以重新执行上述命令启动服务端了,启动完成就可以打开游戏加入了!不过有一个问题,当我们关闭FinalShell远程窗口会话之后,服务端也跟着终止了。我们需要借助screen软件使其后台运行。首先安装screen:sudo apt install screen然后新建一个名为mc的窗口:# screen -S 新窗口名 screen -S mc这个时候你就进入了这个新窗口了,这个窗口和我们远程会话进程是分离的,在这个窗口中,使用cd命令进入服务端核心所在文件夹并执行我们上面启动服务端的一系列操作即可。在这个新窗口启动服务端之后,关闭FinalShell,服务端仍然还在运行,我们就可以正常进入游戏。以后再连接服务器,可以使用screen命令再进入我们的服务端的命令窗口对服务端进行操作:# screen -r 窗口名,进入窗口 screen -r mc4,总结看起来Minecraft开服的步骤很多,其实并不难,总结起来就是:运行服务端 -> 同意eula -> 第一次启动 -> stop -> 修改配置(加mod,删world) -> 再次启动这样就完成了初步配置。以后关闭服务端就是进入服务端运行的screen窗口,执行stop命令停止即可。再启动只需使用java -jar命令运行服务端/Forge服务端核心jar文件即可。如果想给你的MC服务器套上一个域名可以参考:文章附上文一些文件备用下载地址(提取码都是2333):Minecraft原版服务端核心Forge Installer已完成初次配置的Forge服务端,下载后可以直接加入mod就启动
网站邮件发送是一个很实用的功能,例如验证码,通知等等。其实使用Spring Boot发送邮件是一件非常简单的事情。1,配置首先在项目pom.xml文件中添加依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>然后打开Spring Boot配置文件application.properties,在里面加入邮件配置:spring.mail.host=邮件的smtp服务器 spring.mail.username=你的邮箱账户 spring.mail.password=邮箱授权码(注意是授权码不是密码) spring.mail.default-encoding=UTF-8这样就配置好了!前提是你的邮箱需要开启smtp服务,以163邮箱为例:开启任意一个即可,然后按照指引操作,最后会得到一个授权码,建议复制到一个文本文档记下来,这个页面也可以看到smtp地址,复制到配置里面即可:2,发送简单文字邮件在需要调用邮件发送的类中使用@Autowired自动注入JavaMailSender对象,然后创建SimpleMailMessage实例,进行发送即可,下面给出实例:@Value("${spring.mail.username}") private String sender; @Autowired private JavaMailSender mailSender; public void sendNotifyMail(String email, String title, String text) { SimpleMailMessage message = new SimpleMailMessage(); message.setFrom(sender); message.setTo(email); message.setSubject(title); message.setText(text); try { mailSender.send(message); } catch (Exception e) { e.printStackTrace(); } }上面有一个sender变量使用@Value注解注入了我们配置的邮箱值,因为发件人邮箱也需要在Java程序中发送邮件时写,方便起见注入配置文件的值即可而不是再写一遍邮箱地址。以及上面写了个发邮件的方法sendNotifyMail,这里就是发邮件的方法。可见发邮件只需要先自动注入JavaMailSender对象,然后创建SimpleMailMessage实例,并设定SimpleMailMessage实例的发件人、收件人、邮件标题、邮件内容,最后调用JavaMailSender实例的send方法发送SimpleMailMessage实例即可。3,发送富文本(HTML)邮件我们现在见到的很多网站的邮件验证码,都是有一个网页样式的。事实上,我们可以借助JavaMailSender和Thymeleaf模板引擎实现富文本邮件发送。首先记得添加Thymeleaf依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>Thymeleaf模板文件默认位于项目文件夹/src/main/resources/templates目录下,我们在这个目录下创建html的网页模板文件作为邮件模板。Thymeleaf路径是可以修改的,具体修改方法可以在我的其它一篇博客里面找到。我这里在默认模板目录下创建一个网页模板miyakomailtemplate.html如下:<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"> <title th:text="${title}"></title> <style> * { margin: 0; padding: 0; } body { background-color: rgb(224, 224, 224); } .background { position: absolute; width: 390px; height: 450px; background-color: white; } .background .head { position: relative; height: 10px; background-color: rgb(0, 153, 255); } .background .content .title, .text { position: relative; margin-top: 3%; margin-left: 5%; width: 90%; } .background .content .title { color: rgb(0, 153, 255); font-weight: 700; font-size: 30px; } .background .content .text { position: relative; padding: 8px; box-sizing: border-box; margin-top: 3%; margin-left: 5%; height: 275px; border: 1.5px blue solid; overflow: auto; } .background .content .text p { display: inline; border-bottom: 1px green dashed; } .background .miyako { position: absolute; bottom: 14px; right: 5%; display: flex; justify-content: flex-end; align-items: center; width: 300px; } .background .miyako .from { position: relative; margin-right: 16px; font-size: 20px; font-weight: 600; color: rgb(0, 110, 255); } .background .miyako img { position: relative; width: 58px; height: 58px; border: 2px solid blueviolet; border-radius: 50%; } @media (max-width: 768px) { .background { width: 100%; } } </style> </head> <body> <div class="background"> <div class="head"></div> <div class="content"> <div class="title" th:text="${title}">标题</div> <div class="text"> <p th:text="${content}">内容</p> </div> </div> <div class="miyako"> <div class="from">From 出云宫子</div> <img src="https://s6.jpg.cm/2021/10/27/I07fQS.png" /> </div> </div> </body> </html>这个就是我们的网页邮件模板,可以看到,最上面html标签加上了xmlns:th="http://www.thymeleaf.org"表示这是一个Thymeleaf模板文件,然后在模板中,我们可以设定一些变量,给标签加上th:text=${变量名}属性,那么在模板引擎读取时就会把该标签内容渲染成相应的值。当然这些变量需要我们后端发送邮件时先指定变量名和值,最后网页就会渲染成相应的样子。对于富文本邮件网页,我们编写时需要注意:存放我们主体内容的元素(例如我上述中类名为background的div元素)不要去设置它的位置(left,top)!就保持它在默认位置(一般是在body中的left和top都为0的地方)即可,否则收到的邮件显示会异常。如果不写响应式布局,那么存放我们内容的容器也需要用具体数值来规定其宽高。当然,我建议还是做一下移动端适配,在网页head中加上:<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">然后使用媒体查询,适配移动端,这样效果会比较好。媒体查询中存放主体内容的标签就建议把宽度设置为100%。变量部分其实就是Thymeleaf的基本语法,这里不再赘述。现在,我们来编写后端,封装一个方法发送富文本邮件:@Value("${spring.mail.username}") private String sender; @Autowired private JavaMailSender mailSender; @Autowired private TemplateEngine templateEngine; // 注入模板引擎读取模板文件 @Override public void sendHtmlNotifyMail(String email, String title, String content) throws MessagingException { // 通过Context对象构建模板中变量需要的值 Context context = new Context(); context.setVariable("title", title); context.setVariable("content", content); // 传入变量,并渲染模板 String mimeString = templateEngine.process("miyakomailtemplate.html", context); // 创建富文本信息对象 MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, true); helper.setFrom(sender); helper.setTo(email); helper.setSubject(title); // 给helper设置内容为我们渲染的模板内容,第二个参数为true表示内容是html格式 helper.setText(mimeString, true); // 发送邮件 try { mailSender.send(message); } catch (Exception e) { e.printStackTrace(); } }其实大体思路和上面发送简单文本是很相似的,只不过先创建了个Context对象,在这个对象里面储存网页模板中的变量名和对应的变量值(setVariable("变量名", "变量值"),我们上面网页模板中有title和content这两个变量,也可见在Java代码中我们在context这个对象中设定了这两个变量的值),然后使用templateEngine将模板渲染,传入网页模板位置(相对于Thymeleaf目录的位置,这里文件直接放在该目录下,因此传入文件名即可)和Context对象,即可渲染出实际的值。得到网页源代码字符串,发送邮件时将其作为内容发送即可。我们调用该方法测试一下:@PostConstruct public void test() { try { sendHtmlNotifyMail("我的邮箱@163.com", "宫子恰布丁-密码重置", "您的密码重置邮箱验证码为:" + (int) ((Math.random() * 9 + 1) * 100000)); } catch (MessagingException e) { e.printStackTrace(); } }效果:4,将Spring Boot配置到阿里云后无法发送邮件的问题解决我在本地开发时测试发送邮件正常,但是放到阿里云上面就发不出去了。查资料得知因为邮件默认通过25端口发送,而出于安全考虑阿里云默认禁用25端口,因此需要配置加密的465端口才行。我们只需要基于上述配置文件内容继续加入下列配置即可:spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory spring.mail.properties.mail.smtp.socketFactory.port=465 spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.starttls.required=true
使用Windows10的时候常常发现我们没有管理员权限,这对我们使用造成了巨大麻烦。今天我来分享一下Windows10里面怎么获取最高管理员权限。本文同样适用于Windows11一、Windows10专业版/企业版/教育版方法1,把用户添加到Administrators用户组中(新安装Win10创建的第一个账户可以跳过这一步骤)一般如果是新安装的Win10,系统指导你创建的第一个账户他是默认就在Administrators用户组中的,因此新安装Win10创建的第一个账户可以跳过这一步骤。如果是后续手动创建的用户,则需要将其先加入Administrators用户组。(1) 此电脑-右键-管理,找到本地用户和组(2) 对需要赋予管理员权限的用户右键-属性(3) 在属性窗口中,点击隶属于标签,然后点击添加(4) 在下面的“输入对象名称来选择”中输入Administrators,然后点击检查名称(5) 没有弹出错误且名称下面多了下划线说明查找用户组成功,点击确定即可(6) 这样就添加成功了!应用-确定关闭即可。现在还可以移除隶属的Users组2,设定组策略(1) 按下win+R键唤出运行窗口,输入gpedit.msc(2) 这时打开了组策略编辑器,在左边找到计算机配置-Windows 设置,再进入右边安全设置,如图(3) 进入本地策略,如图(4) 进入安全选项,如图(5) 向下滑,找到用户账户控制:以管理员批准模式运行所有管理员和用户账户控制:用于内置管理员账户的管理员批准模式这两项,如图(6) 分别选中并点击鼠标右键,再点击属性,进入配置窗口,将这两项都分别设置为已禁用,再点击“确定”,如图(7) 重启电脑,操作完成!二、Windows10家庭版等其他家庭版系列无法使用组策略,因此除非开启Administrator账户之外,是无法完全获得管理员权限的。只能通过右键-以管理员身份运行。如果右键没有以管理员身份运行,请按照下列步骤操作,有则忽略。(1) 将以下代码复制进txtWindows Registry Editor Version 5.00 [HKEY_CLASSES_ROOT\*\shell\runas] @="获取管理员权限" "NoWorkingDirectory"="" [HKEY_CLASSES_ROOT\*\shell\runas\command] @="cmd.exe /c takeown /f \"%1\" && icacls \"%1\" /grant administrators:F" "IsolatedCommand"="cmd.exe /c takeown /f \"%1\" && icacls \"%1\" /grant administrators:F" [HKEY_CLASSES_ROOT\exefile\shell\runas2] @="获取管理员权限" "NoWorkingDirectory"="" [HKEY_CLASSES_ROOT\exefile\shell\runas2\command] @="cmd.exe /c takeown /f \"%1\" && icacls \"%1\" /grant administrators:F" "IsolatedCommand"="cmd.exe /c takeown /f \"%1\" && icacls \"%1\" /grant administrators:F" [HKEY_CLASSES_ROOT\Directory\shell\runas] @="获取管理员权限" "NoWorkingDirectory"="" [HKEY_CLASSES_ROOT\Directory\shell\runas\command] @="cmd.exe /c takeown /f \"%1\" /r /d y && icacls \"%1\" /grant administrators:F /t" "IsolatedCommand"="cmd.exe /c takeown /f \"%1\" /r /d y && icacls \"%1\" /grant administrators:F /t"(2) 保存然后保存为后缀名reg格式,右键该文件并且选择合并-确认,即可在右键中添加超级管理员权限。
尝试过很多办法去关闭自动更新,但是过几天总是会卷土重来。虽然自动更新更加安全,但对于我们日常用户来说,自动更新总是带来很多麻烦。那么,应当如何有效地关闭自动更新呢?提示:此方法仅仅对专业版/教育版/企业版有效,家庭版暂时没有特别有效的方法。1,Windows7或者Windows10(1) 按下win+r键打开“运行”窗口,输入“gpedit.msc”打开组策略。如图:(2) 依次点击“计算机配置-管理模板-Windows组件”,如图:(3) 在右边找到“Windows Update”(win10系统可能显示的是“Windows 更新”),双击进入,如图:(4) 进入后选择“配置自动更新”,右键-编辑(属性),如图:(5) 在弹出的配置窗口中选择“已禁用”,如图:点“确定”,这样就关闭了自动更新了!此方法能有效关闭自动更新,Windows不会再检测更新。但是用户还是可以去设置(win10)或者控制面板(win7)进行手动更新。教程以win7为例,其他系统(win10)大同小异。2,Windows11Windows11也是差不多的,只不过位置有点不同,还是按照上述步骤打开到Windows更新这一项里面:进入管理最终用户体验:然后就可以看到配置自动更新这一项了!
现在使用java基本上不是8就是11版本。这两个版本设置环境变量方法有所不同,在此做一下总结。一,Windows环境1,java 8安装jdk8之后,找到jdk8安装位置(默认在:C:\Program Files\Java\jdk1.8.0_xxx,xxx表示版本号)然后右键-此电脑-属性-高级系统设置-环境变量。在系统变量一栏点击新建,变量名JAVA_HOME,值指定jdk8安装位置,保存。再新建,变量名classpath,值填入:.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar再打开系统变量中的Path,点击旁边新建,值输入%JAVA_HOME%\bin(win7及其以下用户在值后面加上;%JAVA_HOME%\bin)这样就配置完成了!2,java 11java 11和java 8环境变量配置大同小异,和java 8一样先在系统变量一栏点击新建,变量名JAVA_HOME,值指定jdk11安装位置(默认在C:\Program Files\Java\jdk-11.x.x,xxx表示版本号),保存。然后直接打开系统变量中的Path,点击旁边新建,值输入%JAVA_HOME%\bin(win7及其以下用户在值后面加上;%JAVA_HOME%\bin)。这样就完成了。可见java 11不需要配置classpath变量。二、Linux环境说在前面的是,一般情况下很多教程配置Linux环境变量都是修改/etc/profile文件,但是发现这样其实并不方便,每次打开终端需要source才行并且不好维护。所以说最好的方法是在/etc/profile.d目录下面建立一个脚本,脚本中使用export命令设置全局变量即可。每次终端打开都会加载该目录下所有脚本,这样就实现了系统环境变量设置,且不需要的话直接删除脚本即可。1,java 8先新建一个文件javaPathSetup.sh,在文件里面写入:#!/bin/bash export JAVA_HOME=你的jdk位置 export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar export PATH=$PATH:$JAVA_HOME/bin:$JAVA_HOME/jre/bin一般从官网下载jdk的压缩包,自行解压到一个位置,然后在脚本中使用export命令给JAVA_HOME变量设定为该路径。然后通过chmod +x命令赋予该文件可执行权限,再复制到/etc/profile.d目录下即可。2,java 11同样的新建一个文件javaPathSetup.sh,在其中使用export命令设定环境变量:#!/bin/bash export JAVA_HOME=你的jdk位置 export PATH=$PATH:$JAVA_HOME/bin然后通过chmod +x命令赋予该文件可执行权限,再复制到/etc/profile.d目录下即可。可见jdk 11不用设置CLASSPATH变量了,且不需要把jre目录加入到PATH中。
Linux系统中也有“服务”这一说法,通过服务我们可以便捷地管理一些程序功能,也可以作为程序开机自启的一个手段之一。今天我来分享一下如何创建自己简易的Linux服务。1,新建服务文件每一个服务在Linux有它自己的对应的配置文件,这个文件可以通过文本编辑器编辑,扩展名为xxx.servive(xxx为服务名称)。这些文件位于/usr/lib/systemd/system目录下。在这个目录下新建service文件即可创建我们的服务。文件的内容结构如下:[Unit] Description=服务描述 After=服务依赖(再这些服务后启动本服务) [Service] Type=服务类型 ExecStart=启动命令 ExecStop=终止命令 ExecReload=重启命令 [Install] WantedBy=服务安装设置可见服务配置文件分为[Unit]、[Service]和[Install]三大部分。一般来说有些值是固定的,没有特殊需要我们直接套用即可。例如[Unit]中After的值一般是:network.target remote-fs.target nss-lookup.target。[Install]的WantedBy一般是multi-user.target。[Service]中是主要内容。Type的值有以下几个:simple:这是默认的值,指定了ExecStart设置后,simple就是默认的Type设置除非指定Type。simple使用ExecStart创建的进程作为服务的主进程,在此设置下systemd会立即启动服务。forking:如果使用了这个值,则ExecStart的脚本启动后会调用fork()函数创建一个进程作为其启动的一部分。当初始化完成,父进程会退出。子进程会继续作为主进程执行。oneshot:类似simple,但是在systemd启动之前,进程就会退出。这是一次性的行为。可能还需要设置RemainAfterExit=yes,以便systemd认为j进程退出后仍然处于激活状态。dbus:也和simple很相似,该配置期待或设置一个name值,通过设置BusName=设置name即可。notify:同样地,与simple相似的配置。顾名思义,该设置会在守护进程启动的时候发送推送消息。其实常用的就是simple和forking了。一般来说我们的程序是应用程序前台使用就用simple,后台/守护进程一般是forking。然后就是启动/停止/重启命令,注意这个命令里面调用的程序必须全部使用绝对路径。例如,我的服务器上的redis的Service配置:[Unit] Description=Redis-Server After=network.target remote-fs.target nss-lookup.target [Service] Type=forking ExecStart=/opt/Redis-6.2.1/redis-server /root/RedisData/redis-conf.conf ExecStop=kill -9 $(pidof redis-server) ExecReload=kill -9 $(pidof redis-server) && /opt/Redis-6.2.1/redis-server /root/RedisData/redis-conf.conf [Install] WantedBy=multi-user.target因为redis一般作为后台程序运行所以Type填forking。kill -9 $(pidof redis-server)命令的意思是:先用pidof命令获取指定名称进程的pid再把这个结果传给kill命令终止对应进程。平时终止特定名称的进程时也可以这么写。其实除此之外,service文件还有很多配置项,这里只写出了常用必要的,满足日常需求,其余可以自行搜索学习,这里不再过多赘述。2,启动/停止/重启我们的服务刚刚建立好了我们的服务配置,现在就可以使用了!在此之前需要先使用下列命令让系统重新读取所有服务文件:systemctl daemon-reload然后通过以下命令操控服务:# 启动服务 service 服务名 start # 终止服务 service 服务名 stop # 重启服务 service 服务名 restart那么注意服务名就是我们刚刚创建的服务配置文件service文件的文件名(不包括扩展名),例如我的服务文件是redis-server.service,那么我的服务名是redis-server。其实我们执行启动服务命令时,就会执行我们刚刚配置文件中ExecStart的值的命令,同样终止、重启会对应执行配置文件中ExecStop、ExecReload的值的命令。3,启用/禁用开机自启通过以下命令启用/禁用开机自启动:# 启用开机自启 systemctl enable 服务名 # 禁用开机自启 systemctl disable 服务名
昨天在justhost.ru上面购买了一个外国的服务器,为了“减少成本”,我没有配置ipv4,而只有ipv6。然后发现服务器用apt update无法连接镜像源进行更新。后来查了资料才发现,仅ipv6的服务器是无法解析并连接ipv4的域名或地址的,而大多数镜像源可能还是ipv4的,因此需要修改服务器的dns并配置ipv6镜像源。1,配置服务器的DNS这里给出一些常用公共dns:供应商IPv4IPv6Google8.8.8.8和8.8.4.42001:4860:4860::8888和2001:4860:4860::8844Cloudflare1.1.1.1和1.0.0.12606:4700:4700::1111和2606:4700:4700::1001阿里云223.5.5.5和223.6.6.62400:3200::1和2400:3200:baba::1清华大学101.6.6.62001:da8::666打开/etc/resolv.conf这个文件,可以先将其中清空,然后加入下列内容设定dns:#谷歌 IPv6 DNS nameserver 2001:4860:4860::8888 nameserver 2001:4860:4860::8844 #阿里云 IPv6 DNS nameserver 2400:3200::1 nameserver 2400:3200:baba::1 #Cloudflare IPv6 DNS nameserver 2606:4700:4700::1111 nameserver 2606:4700:4700::1001任选两个粘贴进去即可,国外的话推荐谷歌和Cloudflare。2,配置IPv6镜像源目前找到的就有清华大学镜像源、上海交大镜像源和中科大镜像源支持ipv6,下面贴出Debian 10的镜像配置,其余可以到镜像站官网查看帮助。清华大学:# 清华大学-Debian 10软件源 deb https://mirrors.tuna.tsinghua.edu.cn/debian buster main contrib non-free deb https://mirrors.tuna.tsinghua.edu.cn/debian buster-updates main contrib non-free deb https://mirrors.tuna.tsinghua.edu.cn/debian buster-proposed-updates main contrib non-free deb https://mirrors.tuna.tsinghua.edu.cn/debian buster-backports main contrib non-free deb-src https://mirrors.tuna.tsinghua.edu.cn/debian buster main contrib non-free deb-src https://mirrors.tuna.tsinghua.edu.cn/debian buster-updates main contrib non-free deb-src https://mirrors.tuna.tsinghua.edu.cn/debian buster-proposed-updates main contrib non-free deb-src https://mirrors.tuna.tsinghua.edu.cn/debian buster-backports main contrib non-free # 清华大学-Debian 10安全更新源 deb https://mirrors.tuna.tsinghua.edu.cn/debian-security buster/updates main contrib non-free deb-src https://mirrors.tuna.tsinghua.edu.cn/debian-security buster/updates main contrib non-free中科大:# 中科大-Debian 10软件源 deb http://mirrors.ustc.edu.cn/debian buster main contrib non-free deb http://mirrors.ustc.edu.cn/debian buster-updates main contrib non-free deb http://mirrors.ustc.edu.cn/debian buster-proposed-updates main contrib non-free deb http://mirrors.ustc.edu.cn/debian buster-backports main contrib non-free deb-src http://mirrors.ustc.edu.cn/debian buster main contrib non-free deb-src http://mirrors.ustc.edu.cn/debian buster-updates main contrib non-free deb-src http://mirrors.ustc.edu.cn/debian buster-proposed-updates main contrib non-free deb-src http://mirrors.ustc.edu.cn/debian buster-backports main contrib non-free # 中科大-Debian 10安全更新源 deb http://mirrors.ustc.edu.cn/debian-security buster/updates main contrib non-free deb-src http://mirrors.ustc.edu.cn/debian-security buster/updates main contrib non-free上海交大:# 上海交通大学-Debian 10软件源 deb https://mirror.sjtu.edu.cn/debian buster main contrib non-free deb https://mirror.sjtu.edu.cn/debian buster-updates main contrib non-free deb https://mirror.sjtu.edu.cn/debian buster-proposed-updates main contrib non-free deb https://mirror.sjtu.edu.cn/debian buster-backports main contrib non-free deb-src https://mirror.sjtu.edu.cn/debian buster main contrib non-free deb-src https://mirror.sjtu.edu.cn/debian buster-updates main contrib non-free deb-src https://mirror.sjtu.edu.cn/debian buster-proposed-updates main contrib non-free deb-src https://mirror.sjtu.edu.cn/debian buster-backports main contrib non-free # 上海交通大学-Debian 10安全更新源 deb https://mirror.sjtu.edu.cn/debian-security buster/updates main contrib non-free deb-src https://mirror.sjtu.edu.cn/debian-security buster/updates main contrib non-free配置完DNS和镜像源,就可以使用apt update进行更新,然后安装软件了!
众所周知,fcitx和ibus是两款很好用的Linux中文输入法框架。下面来说一下其安装方法以及会踩的坑。首先fcitx和ibus是不能共存的,两者只能装其一,所以安装其中一个时最好先使用sudo apt purge命令卸载。卸载fcitx:sudo apt purge fcitx* sudo apt autoremove卸载ibus:sudo apt purge ibus* sudo apt autoremove1,安装fcitx系列输入法直接通过apt命令安装即可,下列是安装命令列表:google拼音:sudo apt install fcitx-googlepinyinsun拼音:sudo apt install fcitx-sunpinyin直接执行命令即可,然后会自动安装依赖。然后重启系统就可以了!按Ctrl+空格可以切换输入法。2,安装ibus输入法比起fcitx,ibus安装配置起来可能稍显复杂一点。先安装ibus框架:sudo apt install ibus ibus-libpinyin然后安装对应输入法:ibus智能拼音:sudo apt install ibus-pinyinsun拼音:sudo apt install ibus-sunpinyin重启系统,然后进入设置-语言和区域,找到键盘或者输入源设置:点击+按钮,点击进入汉语-选择对应输入法,添加即可。然后点击任务栏的输入法图标即可切换输入法。
Linux中有许多桌面应用环境,在这其中除了deepin的dde桌面之外,界面和功能都很强大好用的就是kde了。下面我来分享一下我的kde安装经过。我的Linux发行版是deepin的v15.11版本,尝试使用kde作为第二个桌面。1,安装KDE安装方法其实很简单。打开终端,输入下面命令即可:最小安装-仅仅安装桌面和基本组件:sudo apt install kde-plasma-desktop【推荐】标准安装-会安装桌面以及常用软件:sudo apt install kde-standard然后会安装许多软件包,需要耗费较长的时间。在安装过程中会让你选择X登录界面管理器,如果你想用原来的桌面(dde)的登录界面就选择lightdm,也可以选择下面的使用kde的登录界面管理器。后面如果想更换登录管理器可以通过以下命令:sudo dpkg-reconfigure lightdm(登录界面管理器是什么?这个在我原来写的一个讲安装Gnome桌面的博客中有讲到,可以翻翻我的那一篇博客,这里不再过多赘述。)等到安装完成,注销账户,你就可以切换桌面了!2,建议的基本设置在程序 - 设置 - 系统设置中打开系统设置:在桌面行为 - 工作空间中可以设定鼠标动作。习惯windows的话建议点击行为,选择双击文件和文件夹:在锁屏中设定锁屏时间、快捷键、锁屏壁纸等等:然后在开机和关机选项组中可以设置登录屏幕sddm的样式:在桌面会话中设定会话选项,这里可以设定登录时是恢复上一次会话(关机时未关闭的应用会恢复)还是空会话启动。建议选择空会话启动以加快速度。在账户详细信息中设定KDE钱包的开启和关闭、用户名和权限等等:在网络-连接中设定网络dns等等:在电源管理 - 节能中设定各个情况的熄屏、注销等等设定:在桌面空白处右键 - 配置桌面中可以设置桌面图标和壁纸等等:对右下角托盘三角形 - 右键 - 配置 系统托盘可以设定系统托盘的图标显示和隐藏等等:3,安装KDE之后没有wifi的问题如果系统原来可以正常使用wifi,而安装KDE之后就找不到连接wifi的选项了,这时通过以下命令安装KDE的wifi管理包即可:sudo apt install plasma-nm
Gnome是linux下比美观易用的一款Linux图形化界面,发现网上的教程很多,也不一样。我也自己测试了一下,其实操作很简单。下面就开始介绍如何安装并配置好桌面程序。1,安装gnome我们只需要执行一个命令即可:sudo apt install gnome这个过程会花费很长时间,等待其完成。安装过程中会显示让你选择X显示管理器的窗口,选择gdm3,回车确定。安装完成,重启系统即可!如果你安装了多个桌面,要切换桌面,执行:sudo update-alternatives --config x-session-manager按照指示操作,重启系统改变生效。2,修改显示管理器(登录界面)这个在上面安装Gnome过程中就会让你选择了,上面选择了gdm3。这里仅仅了解即可。显示管理器(登录管理器)是一个在启动后显示的图形界面,即登录界面(你输入账号密码登录的图形界面),是进到桌面环境之前的用户登录界面。我们通过这个显示管理器,就可以切换我们的登录界面的样式,例如我们可以切换成Gnome的登录界面,也可以切换使用kali自带的登录界面,两者不同。一般在登录的时候有一个地方可以让你切换,一般切换之后重启生效。不过我之前kali上面用的是lightdm,结果发现使用了lightdm之后就只能使用kali自带的登录界面且无法切换了,于是我切换成gdm3,果然就可以改变登录界面了。切换命令:sudo dpkg-reconfigure gdm3或者是:sudo dpkg-reconfigure lightdm选择后重启生效。3,给Gnome桌面安装插件gnome的插件是通过其网页安装的。进入他的插件官网即可。然后你需要给你的谷歌浏览器安装扩展才能进行插件安装,进入插件官网之后你会看到这个提示,点击就可以安装插件:也可以去Crx4Chrome搜索gnome下载安装插件 :然后我们搜索插件,进入页面后把启用开关打开即可!Gnome建议必备插件:Dash to Dock(侧边工具栏)Applications Menu(应用程序菜单)TopIcons Plus(顶栏托盘图标)在应用程序中打开优化-扩展可以配置各个已安装扩展:如果说找不到优化应用程序则通过下列命令安装:sudo apt install gnome-tweaks
将网站放在服务器后,因为服务器带宽问题,才发现很多资源比如图片、音频在网站打开后1分钟还没有加载,导致无法播放。所以说我决定使用js在最开头加一个判断资源是否加载完成的函数并运行,只有资源全加载完成了才能进入主页面。在网上查了很多都是说用onload,但是这只能判断文档dom树是否解析完成,但是音频、图片等等资源加载完成没是无法判断的。下面我将来分享我的方案。1,判断音频/视频是否加载完成音频,视频元素分别是<audio>、<video>,这两个元素都有readyState属性表示其是否加载完成,我们可以获取其属性值,其属性值对应意义如下:值意义0HAVE_NOTHING - 没有关于音频/视频是否就绪的信息1HAVE_METADATA - 关于音频/视频就绪的元数据2HAVE_CURRENT_DATA - 关于当前播放位置的数据是可用的,但没有足够的数据来播放下一帧/毫秒3HAVE_FUTURE_DATA - 当前及至少下一帧的数据是可用的4HAVE_ENOUGH_DATA - 可用数据足以开始播放那么,判断该属性值为4时,既可以知道加载完成了。与此同时,音频和视频元素可以设定其preload属性设置其是否预加载,属性值如下:值意义auto当页面加载后载入整个音频metadata当页面加载后只载入元数据none当页面加载后不载入音频默认这个值是metadata,只加载元数据。因此如果想要音频即时使用建议修改其属性为auto,然后再来查看其readyState属性来判断加载完成没,但这样网页打开可能会慢一点。例如设定音频为自动加载:<audio src="./testAudio.wav" preload="auto"></audio>在我的一台很慢的服务器上面通过下列js每秒读取100次一个audio标签是否加载完成:setInterval(() => { console.log(document.querySelector('.succeedAduio-2').readyState); }, 10);结果如下:可见一开始没加载好就是0,最后才是4。根据这个我们就可以知道音频组件是否加载完成,加载完成了才能播放。视频元素也是一样的。2,图片img元素img元素不像音视频元素,它没有readyState这个属性,不过可以通过其complete属性来获取。当图片加载完成时,该属性值为true,否则为false。例如我这里有个img元素:<img src="https://file.moetu.org/images/2021/03/03/gz-13c176dbd1ced48543.png" />还是向上面一样,我用js每秒读取100次该属性:let img = document.querySelector('img'); //获取img元素 setInterval(() => { console.log(img.complete); }, 10);结果如下:可见对于img元素,通过其complete属性即可知道是否加载完成。3,div元素的背景图片div没有上述两个属性,但是对于有背景图片的div元素来说,又应该怎样才能知道其是否加载完成呢?我们可以通过js获取div的style.backgroundImage的值来获取div的背景图片的地址,再以该地址为参数在js创建一个Image对象,Image对象也有complete属性,根据新建对象来判断是否加载完成。在此先写一个函数获取div的背景图片地址:/** * 获取div元素的背景图片地址 * @param {*} divElement div元素 */ function getDivImage(divElement) { let imgurl = window.getComputedStyle(divElement, null).getPropertyValue('background-image'); return imgurl.substring(5, imgurl.lastIndexOf('\"')); }需要说明的是,window.getComputedStyle方法可以获取元素所有样式,具体用法可以去MDN上面查看。例如我有一个div元素:<div class="divImg"></div>它已经通过css设定了背景图片:.divImg { position: relative; width: 200px; height: 150px; background: url("https://file.moetu.org/images/2021/03/03/ys-4b588a903494b1e90.png") no-repeat center/cover; }然后我在js中获取这个元素,并通过上述自定义函数获取其背景图片地址,创建Image对象,并每秒读取100次是否加载完成:let divImg = document.querySelector('.divImg'); //获取div元素 let getImg = new Image(); //新建Image对象 getImg.src = getDivImage(divImg); //给Image元素指定地址 setInterval(() => { console.log(getImg.complete); }, 10);结果如下:上述测试图都很大,所以可见这加载了大几十秒甚至1分钟才好。4,资源预加载问题要想实现资源预加载其实很简单,视频音频元素上面有提到,改其preload属性为auto即可。而图片预加载只需要在js里面创建一个Image对象,并指定其src即会预加载图片。例如我要预加载我所需的所有图片://将所需的图片地址存入一个数组 const imgurls = ['https://file.moetu.org/images/2021/03/04/gz-1844d1db4a9a2d0dc3.png', 'https://file.moetu.org/images/2021/03/04/ys-5627f9c93bc08ad29.png', 'https://file.moetu.org/images/2021/03/03/ys-4b588a903494b1e90.png', 'https://file.moetu.org/images/2021/03/03/gz-13c176dbd1ced48543.png', 'https://file.moetu.org/images/2021/02/10/illust_85719128_20201120_19253792efd4d4d825d6e1.jpg' ]; //遍历数组中的图片地址并创建Image对象 for (let i = 0; i < imgurls.length; i++) { let img = new Image(); img.src = imgurls[i]; }上述代码中,我们只是用js循环遍历图片地址依次创建Image对象。其实在创建了Image对象并设定了其src属性后,图片就开始加载了!加载好后HTML中调用就无需再次加载,直接使用。在一个没有写任何元素的HTML中调用这个js脚本执行上诉语句,可以发现虽然页面不显示任何东西,但是已经开始加载图片资源了:预加载后资源可以即时使用,但是也会相应地使网站速度打开变慢,所以一般不要预加载太多资源。
Redis是现在最受欢迎的NoSQL数据库之一,最近也开始学习这个Redis了。所以我决定来分享一下Linux上的Redis的安装和配置。1,下载Redis源码并解压在官网下载页面下载源码,如图: 备用地址,提取码2333我这里下载了一个redis-6.0.8.tar.gz的文件,解压至当前目录并进入解压的文件夹(命令中操作的文件名根据自己下载的而定,此处命令以我自己的为例):tar -xvf redis-6.0.8.tar.gz cd redis-6.0.82,编译源代码首先需要确定的是我们的机器上安装了gcc,make,libc6-dev这几个软件包(如果是32位Linux或者需要编译32位redis时还需再安装libc6-dev-i386或者g++-multilib)。可以通过以下命令安装(已安装这些软件包的可以忽略这一步):# 只编译64位Redis sudo apt install gcc make libc6-dev # 需要编译32位的Redis sudo apt install gcc make libc6-dev libc6-dev-i386若需要编译32位Redis,安装依赖时找不到libc6-dev-i386,那就把上面安装命令中libc6-dev-i386换成g++-multilib。然后开始编译Redis。刚刚已经解压并进入其源码目录了,现在执行以下命令编译:make如果想在64位机器上编译32位的Redis可执行文件,则执行:make 32bit等待编译完成,显示Hint: It's a good idea to run 'make test' ;)字样时说明编译成功了。若在编译中遇到任何错误需要再重新编译,需要先清理已编译部分,执行命令:make distclean然后就可以重新make了。然后通过以下命令安装Redis到系统:sudo make install若想把Redis安装至指定的位置,可以使用如下命令:make PREFIX=想要安装到的位置(绝对路径) install例如安装到/home/swsk33/redis:make PREFIX=/home/swsk33/redis install这时,redis便编译并安装完成了!3,编写配置文件并启动Redis这个时候其实通过直接输入redis-server便可以直接启动了。redis可以指定配置文件运行,最好是通过配置文件启动。先自己新建一个文件,例如redis-config.conf,自己加入配置内容。常见的配置如下:配置项说明daemonize yes/noRedis 默认不是以守护进程的方式运行,可以通过该配置项修改,使用 yes 启用守护进程打开即启动后使redis服务端后台运行pidfile pid文件位置当 Redis 以守护进程方式运行时,Redis 默认会把 pid 写入 /var/run/redis.pid 文件,可以通过 pidfile 指定port 自定义端口号指定 Redis 监听端口,默认端口为 6379bind 绑定的ip地址绑定的主机地址。若不写这一行,则外网所有的电脑都可以连接此服务器的redistimeout 毫秒当客户端闲置多长秒后关闭连接,如果指定为 0 ,表示关闭该功能loglevel 日志级别指定日志记录级别,Redis 总共支持四个级别:debug、verbose、notice、warning,默认为 noticelogfile 日志文件位置指定日志文件位置,默认不输出日志到文件dbfilename 数据库名.rdb指定本地数据库文件名,默认值为 dump.rdbdir 数据库存放目录指定本地数据库存放目录,默认存放在当前目录下requirepass 密码设置 Redis 连接密码,如果配置了连接密码,客户端在连接 Redis 时需要通过 auth 密码命令提供密码,默认关闭例如设置密码为123456:requirepass 123456maxclients 最大连接数设置同一时间最大客户端连接数,默认无限制,Redis 可以同时打开的客户端连接数为 Redis 进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis 会关闭新的连接并向客户端返回 max number of clients reached 错误信息maxmemory 最大内存占用字节数指定 Redis 最大内存限制,Redis 在启动时会把数据加载到内存中,达到最大内存后,Redis 会先尝试清除已到期或即将到期的 Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis 新的 vm 机制,会把 Key 存放内存,Value 会存放在 swap 区vm-enabled yes/no指定是否启用虚拟内存机制,默认值为 no,简单的介绍一下,VM 机制将数据分页存放,由 Redis 将访问量较少的页即冷数据 swap 到磁盘上,访问多的页面由磁盘自动换出到内存中上面配置不需全部写入。例如我的配置文件如下:port 25002 dir /root/Temp/db requirepass 12345678建议requirepass最好写上保证安全。如果有需要可以用bind配置绑定ip地址,这样只有绑定的ip地址才能访问redis数据库。然后启动redis服务端:redis-server 配置文件位置例如我的:redis-server /home/swsk33/redis/redis-config.conf若上面make install时使用了自定义的安装位置(使用了PREFIX参数),那么需要进入你的安装目录下的bin文件夹里面再执行命令:./redis-server 配置文件路径显示这个画面说明启动成功:服务器上建议使用screen软件新建一个窗口在里面运行redis服务端,这样可以使其挂在服务器上面运行。screen的使用此处不再赘述。4,远程连接redis服务端远程连接时须确保没有配置bind值或者bind值是你的ip,且端口开放。远程连接也需要在自己的电脑上编译并安装redis。Windows编译可自行百度,方法类似。此处以Linux电脑远程连接为例,通过以下命令连接:redis-cli -h 服务器ip -p redis的端口本地连接也是这个命令,ip地址是127.0.0.1连接上后会进入redis命令行:然后使用AUTH命令输入密码认证:auth redis密码密码就是前面配置文件中requirepass的配置值。输出OK说明连接认证成功!最后使用quit或者exit命令断开连接。
MySQL是用的很多的关系型数据库。今天来分享一下安装,配置及其连接教程。这里以Debian服务器安装MySQL 8为例。1,下载MySQL并解压上传去下载页面下载linux版的mysql安装包。注意安装之前必须完全卸载MariaDB,先执行:sudo apt purge mariadb*然后下载了一个tar文件,解压你会发现解压出来了一堆deb文件,把这些文件全部上传到linux服务器的一个目录里,并cd命令进入那个目录,然后安装所有的deb文件:sudo dpkg -i *.deb若安装后有错误信息,则执行:sudo apt update sudo apt install -f期间会显示一个界面让你设置root密码,即数据库超级管理员root的密码,设置即可:输入密码,ok,然后重复输入密码确认之后会告知你mysql8用了新加密方式,ok即可:然后会让你选择加密方式,有新加密方式和传统加密方式两种。这里需要说明的是如果选择新加密方式可能远程连接数据库时会报错,所以没特殊需要建议选择传统加密方式:这样就安装完成了!2,修改配置文件其实通常情况下配置文件不需要修改,不过有特殊需要的话就建议修改一下。mysql 8的配置文件路径是/etc/mysql/my.cnf。不过我们一般只需修改服务端的配置即可。服务端配置需要修改/etc/mysql/mysql.conf.d/mysqld.cnf文件,打开这个文件可以看到:# Copyright (c) 2014, 2017, Oracle and/or its affiliates. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License, version 2.0, # as published by the Free Software Foundation. # # This program is also distributed with certain software (including # but not limited to OpenSSL) that is licensed under separate terms, # as designated in a particular file or component or in included license # documentation. The authors of MySQL hereby grant you an additional # permission to link the program and your derivative works with the # separately licensed software that they have included with MySQL. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License, version 2.0, for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # # The MySQL Server configuration file. # # For explanations see # http://dev.mysql.com/doc/mysql/en/server-system-variables.html [mysqld] pid-file = /var/run/mysqld/mysqld.pid socket = /var/run/mysqld/mysqld.sock datadir = /var/lib/mysql log-error = /var/log/mysql/error.log里面设定了一些默认的数据库文件、日志文件位置,可自行修改。上面[mysqld]表示一个配置组,即为服务端配置,这个配置组下面的四行配置依次代表:进程文件位置、socket文件位置、数据库存放文件夹和错误日志文件位置。下面来说几个常用配置。(1) MySQL端口服务端的默认端口是3306,不过在这个文件这可以添加配置修改其端口,接着加入下列语句(在[mysqld]配置组):port=自定义端口号(2) 字符集然后就是字符集,建议改成utf8mb4,现在上面服务端配置组[mysqld]里面加入:character-set-server=utf8mb4再修改/etc/mysql/my.cnf这个配置文件,加入[client]配置组(如果没有的话)并设置字符集,添加如下语句:[client] default-character-set=utf8mb4(3) 解决连接MySQL非常慢的问题如果说安装了MySQL,然后通过命令行或者Navicat远程连接的时候,都要等少好多秒才能连接上,连接的非常慢的话,可以关闭其域名解析功能。编辑/etc/mysql/mysql.conf.d/mysqld.cnf文件,在[mysqld]配置组下加入如下一行配置即可:skip-name-resolve修改完成配置后记得重启mysql服务(见下面3)。3,启动数据库服务通过这个目录启动:service mysql start还可以停止或者重启:停止:service mysql stop重启(修改完配置文件重启生效):service mysql restart安装完成,MySQL的服务端是默认开机自启动的,我们可以通过下列命令打开/关闭MySQL的开机自启动:允许开机自启:systemctl enable mysql禁用开机自启:systemctl disable mysql4,设置外网可以连接数据库在服务器上面输入命令连接数据库:mysql -u root -p然后输入管理员密码,就连接成功了。然后依次执行下列命令:use mysql; update user set host='%' where user='root'; flush privileges;就设置成功了。5,Windows上连接MySQL我们在自己的电脑上也要安装MySQL,这个从官网下载Windows MySQL即可。安装配置方法很简单,百度即可,这里不再赘述。(一般是解压,然后把其中bin文件夹所在文件添加至Path环境变量)配置好后打开cmd,输入命令:mysql -h 服务器地址 -P 端口(服务端使用默认端口3306可以不要这个-P选项) -u 数据库用户名 -p接着要输入密码,然后就可以了。6,MySQL数据库常规操作登录数据库后,我们要进行添加用户、数据库等操作。(1)添加用户create user '用户名'@'可接受连接的主机的ip' identified by '用户密码';下面给出几个例子:创建用户a密码为123456,a只能从本机登录(主机字段为localhost):create user 'a'@'localhost' identified by '123456';创建用户b密码为123456,b可以从外网任何主机远程登录(主机字段为%):create user 'b'@'%' identified by '123456';然后授权用户:grant 权限 on 授权可操作的数据库.授权可操作的表名 to '被授权用户'@'可操作的主机ip';其中权限字段有SELECT, INSERT, UPDATE等,若授予所有权限则这里填ALL。例如给b授予操作所有数据库的所有表的全部权限:grant ALL on *.* to 'b'@'%';需要注意的是,用以上命令授权的用户不能给其它用户授权,如果想让该用户可以授权,用如下的命令:grant 权限 on 授权可操作的数据库.授权可操作的表名 to '用户名'@'可操作主机' with grant option;(2)修改用户密码在MySQL8中,使用alter语句修改密码。先登录,然后执行以下语句:alter user '用户名'@'授权的登录地址' identified by '新密码';注意登录root或者任何高权限账户可以修改自己以及其他用户密码,而普通账户登录只能修改自己的密码。授权的登录地址值即为创建账户的时候设定的值(仅本机访问是localhost,全部可以访问是%)。(3)新建数据库create database 数据库名;若要指定新建数据库的编码和排序字符集,则使用:create database 数据库名 character set 编码 collate 排序字符集;例如:create database test character set utf8mb4 collate utf8mb4_unicode_ci;最后切换至数据库:use 数据库名;
有些时候由于连不上git服务器而我们又需要推送代码,这时就需要设定git代理服务器。1,http和https代理如果说使用的是项目http或者https地址,就配置http与https代理即可,输入以下命令:git config --global http.proxy "socks5://地址:端口" git config --global https.proxy "socks5://地址:端口"例如设定本地代理:git config --global http.proxy "socks5://127.0.0.1:1080" git config --global https.proxy "socks5://127.0.0.1:1080"这样使用git clone/push/pull所有http或者https地址项目都会走代理。还可以使用下面命令取消代理设置:git config --global --unset http.proxy git config --global --unset https.proxy2,ssh代理设定如果说项目使用的ssh地址,那么就需要配置ssh代理。我们需要编辑ssh的配置文件,位于用户文件夹下的.ssh文件夹下。Windows ssh配置文件路径:C:\Users\你的用户名\.ssh\configLinux ssh配置文件路径:/home/你的用户名/.ssh/config使用文本编辑器打开配置文件config加入下列配置:Windows系统:ProxyCommand connect -S 代理地址:端口 %h %pLinux系统:ProxyCommand nc -X connect -x 代理地址:端口 %h %p如果说.ssh文件夹不存在或者config文件不存在就自己创建一个。配置好了,ssh就会走代理了。上面是配置全局走代理,事实上一般只需要为指定网址配置代理,例如只为github配置代理,就在配置文件加入:Windows系统:Host github.com ProxyCommand connect -S 代理地址:端口 %h %pLinux系统:Host github.com ProxyCommand nc -X connect -x 代理地址:端口 %h %pHost后面接的就是指定要走代理的地址,可以接多个地址例如:Windows系统:Host github.com gitlab.com ProxyCommand connect -S 代理地址:端口 %h %pLinux系统:Host github.com gitlab.com ProxyCommand nc -X connect -x 代理地址:端口 %h %p可见多个地址使用空格隔开放在Host后面即可,这个例子就是同时指定ssh访问github和gitlab时走代理。例如配置ssh访问github走本地代理:Windows系统:Host github.com ProxyCommand connect -S 127.0.0.1:1080 %h %pLinux系统:Host github.com ProxyCommand nc -X connect -x 127.0.0.1:1080 %h %p
在使用gitee或者github的时候,除了通过账户密码认证以访问仓库,更加推荐和安全的做法还是使用ssh密钥。1,本地生成ssh密钥对安装完成git之后打开git bash或者命令行,输入命令:ssh-keygen -t rsa -C "密钥名"密钥名自己取,可以是邮箱也可以是随意的命名。然后连按三次回车,密钥对就生成了!在C:\Users\你的用户名\.ssh目录下可以看到生成的密钥文件:2,添加公钥到gitee/github上面首先找到我们刚刚生成的公钥文件id_rsa.pub,在C:\Users\你的用户名\.ssh目录下,使用文本编辑器打开id_rsa.pub文件并复制里面全部内容,这些内容即为公钥内容,需要配置到gitee或者github里面。下面分别讲解gitee和github里面配置。(1),gitee在个人账户设置里面找到ssh公钥:标题随便,公钥内容就是我们刚刚复制的公钥文件内容。确定添加即可。以后git clone或者remote add项目的地址就用项目ssh地址代替:(2),github其实方法差不多,在账户设置(settings)-SSH and GPG keys这一栏:点击new ssh keys:以后就使用项目ssh地址代替clone/remote地址:配置ssh密钥,第一次执行推送或者拉取命令时可能会出现the authenticity of host 'xxx.com (xxx.xxx.xxx.xxx)' can't be established.的提示,这个不影响使用,根据提示,直接输入yes然后回车即可。下一次执行命令就不会有这样的提示了。3,如果换了电脑仍然想使用原来的公钥如果说换了一台电脑,那就不能使用原来的公钥push/pull了,除非重复步骤1,2在新电脑上生成新的密钥对。不过我们不需要这样做,其实把原来的电脑上的密钥文件拷贝到U盘或者一些移动介质里面,再放到新电脑的密钥位置即可。把原来生成密钥的电脑上的密钥,即位于C:\Users\你的用户名\.ssh的两个文件id_rsa和id_rsa.pub,拷到新电脑的C:\Users\新电脑用户名\.ssh目录下即可。linux系统拷贝到/home/你的用户名/.ssh文件夹下即可,用户文件夹下没有.ssh文件夹就创建一个。说白了只要把公私钥文件放在用户目录下的.ssh文件夹下即可。4,提示远程密钥变化报错问题解决有时我们在推送/拉回代码时,可能会出现以下情况导致我们操作失败:这是由于验证远程证书失败导致。我们还是找到用户文件夹中的.ssh文件夹,即位于C:\Users\你的用户名\.ssh下,找到其中的known_hosts和known_hosts.old,将这两个文件删除即可。然后再重新操作就不会出现这个问题了!known_hosts文件是用于记录远程仓库的地址和公钥的文件
文件上传是网页常见的一个表单提交形式。实质上,文件上传是前端发送一个POST请求,后端接收即可。不过在Spring Boot中怎么实现文件上传呢?一、上传单个文件(1) 前端先做一个简易的表单,代码如下:<form enctype="multipart/form-data" method="POST" action="/upload"> <input type="text" name="imgName" /> <input type="file" name="imgFile" /> <input type="submit" value="upload" /> </form>需要注意的是我们如果要上传文件,必须设定表单为multipart/form-data格式,上面的标签中的enctype属性就是设定表单数据格式的属性。即指定了表单该条目为文件类型。如果不想使用form标签中的按钮,而是自己组装数据,可以写如下函数:/** * 上传文件函数 * @param {*} requestURL 请求地址 * @param {*} file 文件(input对象的文件) */ function upload(requestURL, file) { let form = new FormData(); //新建表单对象 form.append('img', file); //向表单对象添加文件,名字为img fetch(requestURL, { method: 'POST', body: form }).then((response) => { return response.json(); }).then((result) => { //请求完成后对结果的处理 console.log(result); }) }其中获取input标签中的file:let input = document.querySelector('input'); //查询到input标签 let file = input.files[0]; //input的属性files表示选择的文件数组,如果是单文件就指定下标0注意可以不用指定fetch中的headers的content-type,发送FormData对象可以自行适配。(2) 后端先在Spring Boot配置文件application.properties中配置文件上传大小限制,因为其默认限制是1mb,所以上传文件稍大就会失败:# 设置内置Tomcat请求大小为20MB server.tomcat.max-http-form-post-size=20MB # 设置请求最大大小为20MB spring.servlet.multipart.max-request-size=20MB # 设置文件上传最大大小为20MB spring.servlet.multipart.max-file-size=20MB三个选项最好是都设定一下,如果想无限制可以都填-1。然后在Controller类中写如下方法:@PostMapping("/upload") public String upload(@RequestParam("imgFile") MultipartFile file, @RequestParam("imgName") String name) throws Exception { // 设置上传至项目文件夹下的uploadFile文件夹中,没有文件夹则创建 File dir = new File("uploadFile"); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir.getAbsolutePath() + File.separator + name + ".png")); return "上传完成!文件名:" + name; }需要注意的是,上传过来的文件在Java中是MultipartFile类型,@RequestParam中的值即为我们前端表单每一项(input标签)里面的name属性值,或者是使用FormData对象append时对应的那个名字,要一一对应,必须相同。前端表单中的action属性即为表单提交至的地址,对应我们Controller的@PostMapping中的值。MultipartFile实例通过使用方法transferTo方法实现把上传的文件保存至指定位置。效果:文件也上传至了指定位置:注意最好是使用@RequestParam逐个接受参数,因为@RequestBody不支持multipart类型,参数多了可以分接口接收。二、上传多个文件上传多个文件其实也很简单,和上面上传单个文件差别不大。在前端的这一条表示文件的条目标签中加入属性multiple即可,我的代码如下:<form enctype="multipart/form-data" method="POST" action="/upload"> <input type="text" name="imgColName" /> <input type="file" name="imgFile" multiple /> <input type="submit" value="upload" /> </form>这时,再上传文件时后端接收到的是MultipartFile数组,因此在上述的Controller方法中的对应的形参改成MultipartFile[]类型即可,然后遍历操作,具体例子如下:@PostMapping("/upload") public String upload(@RequestParam("imgFile") MultipartFile[] files, @RequestParam("imgColName") String name) throws Exception { File dir = new File("uploadFile"); if (!dir.exists()) { dir.mkdirs(); } for (MultipartFile file : files) { file.transferTo(new File(dir.getAbsolutePath() + File.separator + file.getOriginalFilename())); } return "上传完成!图集名:" + name; }示例程序仓库地址
文件系统是我们开发过程中常常会接触的问题。那么在Spring Boot框架中,文件的访问又是什么样的呢?今天在此做一个总结。1,file和classpath存放在电脑上实际位置的文件,在Spring Boot中用file:开头表示。例如:file:a.txt 当前目录下的a.txt文件。当前路径在开发环境下一般为Maven项目的目录下(与pom.xml同目录下),在打包为jar文件后当前路径即为运行jar文件时的运行路径。file:D:\a.txt 表示绝对路径,在此不多赘述。而在jar文件内部中,我们一般把文件路径称为classpath,所以读取内部的文件就是从classpath内读取,classpath指定的文件不能解析成File对象,但是可以解析成InputStream。例如:classpath:/a.txt jar包根目录下的a.txt。classpath以/开头表示绝对路径,即为jar包根目录。2,Spring Boot的静态资源访问我们都知道Spring Boot工程文件夹中的src/main/resources是用于存放资源的地方。默认时Spring Boot打包之后静态资源位置如下:classpath:/staticclasspath:/publicclasspath:/resourcesclasspath:/META-INF/resources在Spring Boot中classpath的根目录就对应工程文件夹下的src/main/resources。可以先看这个例子:在工程文件夹下src/main/resources/static下放入图片qiqi.png:运行,访问127.0.0.1:8080/qiqi.png,效果如下:这个例子可见外部访问的资源路径和Spring Boot工程中资源文件路径的一一对应关系。即外部访问时的“根目录”即对应着上述的四个静态资源位置(classpath)。还可以新建一个Controller类,写如下方法:@GetMapping("/pic") public String showPic() { return "/qiqi.png"; }运行,访问127.0.0.1:8080/pic,效果同上。在这个Controller方法中,上面@GetMapping是路由路径,return的是对应的资源路径。其实这个默认的资源路径是可以修改的。我们需要知道在配置文件application.properties中可以加入下列两个配置项:spring.mvc.static-path-pattern spring.web.resources.static-locations我们来逐一进行讲解。(1) spring.mvc.static-path-pattern - 指定资源访问路径这个spring.mvc.static-path-pattern代表的是应该以什么样的路径来访问静态资源,也就是只有静态资源满足什么样的匹配条件,Spring Boot才会处理静态资源请求。说白了就是资源的外部访问路径。根据上述例子我们知道了这个配置默认值为/**。假设在上述工程配置文件中加入:spring.mvc.static-path-pattern=/resources/**那么再访问我们那个图片就要访问网址:127.0.0.1:8080/resources/qiqi.png好了,我们如果现在想使用Controller类的@GetMapping进行路由的话,如果还是像上面那么写:@GetMapping("/pic") public String showPic() { return "/qiqi.png"; }访问127.0.0.1:8080/pic,你会发现:为什么这时就不行了呢?这是因为我们改变了spring.mvc.static-path-pattern配置的值,那么我们对应的Controller类方法中的返回值,也要对应改变。之前spring.mvc.static-path-pattern没有配置那默认就是/**,那访问/qiqi.png就可以找到图片。现在这个配置改为/resources/**,那很显然要访问/resources/qiqi.png了,再访问/qiqi.png当然访问不到了!因此对应的Controller类方法中也要做出对应修改。上述配置spring.mvc.static-path-pattern为/resources/**,那么我们修改Controller方法如下:@GetMapping("/pic") public String showPic() { return "/resources/qiqi.png"; }可见返回值改成了/resources/qiqi.png。可见,Controller类中的方法的返回值,并非是资源文件的实际的相对路径,而是对应的资源的外部访问路径。这一点也是我和我身边许多朋友容易混淆的一点。(2) spring.web.resources.static-locations - 指定静态资源查找路径再者,spring.web.resources.static-locations用于指定静态资源文件的查找路径,查找文件是会依赖于配置的先后顺序依次进行。根据上述例子可见这个值默认是:classpath:/static,classpath:/public,classpath:/resources,classpath:/META-INF/resources其实上面提到了Spring Boot默认资源文件位置,实质上就是这个配置的值。假设在上述工程中配置文件写入:spring.web.resources.static-locations=classpath:/myRes那么我们要将qiqi.png放在项目文件夹的src\main\resources\myRes文件夹中,才能访问:配置此项后,默认值将失效!还可以使用磁盘路径例如:spring.web.resources.static-locations=file:res即指定资源文件在项目文件夹中的res目录中(即打包后运行jar文件时的运行路径下的res文件夹中)。也可以使用绝对路径。通俗地讲,spring.mvc.static-path-pattern配置指定了我们外部访问的路径,而访问这个外部路径时就会去spring.web.resources.static-locations配置的路径中找对应的资源。(3) 集成Spring Security之后导致上述配置失效今天在维护一个项目的时候发现:即使是正确配置上述的静态资源配置,访问静态资源时一直报404,我也很纳闷:之前好好的啊!怎么就不行了呢?经查阅各种资料发现:若配置了拦截器,则会导致上述配置失效。这个项目使用了Spring Security,可能是因为其中的拦截器配置导致这个资源配置失效了。之前一个项目配置了Swagger之后也出现了这个配置失效的问题,我想应该是同一个原因导致。经参考官方文档之后,发现还有一个方式可以配置静态资源访问路径和对应位置。我们新建一个配置类,重写WebMvcConfigurer中的addResourceHandlers方法即可。因此,在一些外部依赖自带拦截器的情况下,就很有可能覆盖我们上述资源路径配置,导致我们上述资源配置失效。因此这个时候,我们就不能通过写上述配置文件的方式配置静态资源访问了!就要通过写配置类的方式。我们先来看一个例子,我这里项目目录结构如下:然后我们新建一个软件包config,在里面写配置类如下:package com.example.resourcetest.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * 自定义MVC配置器 */ @Configuration public class MyWebMvcConfig implements WebMvcConfigurer { /** * 重写资源路径配置 */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/image/**").addResourceLocations("file:res/image/"); } }可见我们只需要调用addResourceHandlers方法参数registry的方法也可以实现资源路径配置,配置效果和上面是一样的。其中两个方法的意义如下:addResourceHandler 等同于上述配置spring.mvc.static-path-pattern,代表的是应该以什么样的路径来访问静态资源addResourceLocations 等同于上述配置spring.web.resources.static-locations,用于指定静态资源文件的查找路径同样地,配置此项后,默认的资源搜索路径将失效!可见我们要先调用addResourceHandler再调用addResourceLocations,两者是一一对应的,上述代码意思就是:外部访问路径/image/xxx时,就会去当前路径下res/image/目录下找xxx。现在,访问127.0.0.1:8080/image/gz-12.png,可见访问成功:可见通过配置类的方式,我们仍然可以实现上述在配置文件中实现的效果。不过这里需要注意的是,和配置文件中不同,指定静态文件查找路径时,若路径是个目录则必须以/结尾!否则也会出现404的情况。当然,在addResourceHandler和addResourceLocations方法中都可以添加多个路径,例如:registry.addResourceHandler("/image/**").addResourceLocations("file:res/image/", "classpath:/static/");也就是说外部访问/image/xxx时,会去当前目录下res/image/和类路径/static/中去寻找xxx,上面也讲了类路径classpath了,这里对应的也是一样的。还可以这样:registry.addResourceHandler("/image/**", "/img/**").addResourceLocations("file:res/image/");也就是写了多个外部访问路径,表示访问/image/xxx和/img/xxx时,都会到当前路径下的res/image/下去查找xxx。当然,事实上配置类配置的方式也会更加高级,上述配置文件中我们只能配置一个外部访问路径,对应其它多个实际资源查找路径。而在配置类中,我们可以定义多个外部访问路径,对应不同的资源查找路径,例如我将代码改如下:package com.example.resourcetest.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * 自定义MVC配置器 */ @Configuration public class MyWebMvcConfig implements WebMvcConfigurer { /** * 重写资源路径配置 */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 访问/image/xxx对应去res/image查找资源 registry.addResourceHandler("/image/**").addResourceLocations("file:res/image/"); // 访问/web/xxx对应去res/web下查找资源 registry.addResourceHandler("/web/**").addResourceLocations("file:res/web/"); } }这时,我访问http://127.0.0.1:8080/image/gz-12.png可以访问到res/image中的图片,然后访问http://127.0.0.1:8080/web/test.html可以访问到res/web中的网页。可见,配置类提供了更加灵活的配置方式,还能够解决我们配置被其它依赖覆盖的问题。需要注意的是,通常一个项目只能有一个类实现WebMvcConfigurer,否则会造成覆盖产生问题。除了这里配置静态资源之外,之前做用户登录的拦截器也要实现这个接口中的方法,这些方法是可以写在一个配置类中的,毕竟实现的是一个接口。然后同样地,如果是要自定义Controller路由资源,也要和上述配置文件中的一样注意return的访问路径问题。例如我要自定义res/image中图片访问路径,由于配置了addResourceHandler("/image/**"),那对应的Controller方法如下:@GetMapping("/rabbit-halloween") public String image() { return "/image/gz-12.png"; }然后访问http://127.0.0.1:8080/rabbit-halloween也可以访问到图片。所以说如果发现配置文件配置资源路径不起作用,我们就可以删掉配置文件中的相关配置,通过编写配置类的方式来实现资源路径自定义,包括更加灵活的情况下例如需要多个访问路径对应各自不同的资源文件查找路径,也需要用到配置类方式。3,Spring Boot的配置文件位置指定我们也知道Spring Boot的配置文件默认是位于classpath:/application.properties,默认会被打包进jar文件。其实我们也可以修改这个配置文件的位置。在我们的Spring主类上加入如下注解:@PropertySource(value={"自定义配置文件路径"})value表示配置文件位置,也可以填多个:@PropertySource(value={"配置1路径", "配置2路径"})此处以我的主类全部代码为例:package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.PropertySource; @SpringBootApplication @PropertySource(value={"file:self.properties"}) public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }即定义配置文件为项目文件夹下的self.properties文件(打包后运行jar文件时的运行目录下的self.properties文件)。4,多环境的配置文件外置方案我们知道,默认情况下,我们可以在src/main/resources文件夹下创建多个配置文件以对应多个不同环境下的配置,方便灵活切换,例如开发-生产环境下,我们一共有三个配置文件:application.properties application-dev.properties application-prod.properties然后在主配置文件application.properties里面配置一个配置项,即可一键切换配置环境:# 指定当前使用开发环境配置文件 spring.profiles.active=dev我们默认的配置文件名是application,因此多环境的情况下,配置文件命名如下:application-环境配置名.properties主配置文件就是application.properties,在里面配置:spring.profiles.active=环境配置名运行时即可使用指定环境的配置文件。这个时候想配置文件外置,如果按照上述第3部分的方法来,发现就不行了。那么多环境配置的情况下,配置文件如何外置呢?我们需要先知道,其实Spring Boot会默认在这四个位置扫描配置文件:file:./config/ file:./ classpath:/config/ classpath:/我们可以指定spring.config.location属性,来实现自定义Spring Boot的配置文件扫描路径。spring.config.location属性不仅可以设定扫描指定的配置文件,还可以指定扫描指定文件夹。在我们的main方法中最开头,使用System.setProperty方法即可设定,下面给几个例子:// 扫描项目文件夹(jar运行目录)中Resources/config目录中所有的配置文件 System.setProperty("spring.config.location", "file:Resources/config/"); // 扫描项目文件夹(jar运行目录)中Resources/config/app.properties文件 System.setProperty("spring.config.location", "file:Resources/config/app.properties"); // 扫描项目文件夹(jar运行目录)中Resources/config目录和Resources/config2目录中所有的配置文件 System.setProperty("spring.config.location", "file:Resources/config/, file:Resources/config2/");可见spring.config.location属性比较灵活,既可以设定文件还可以指定文件夹,注意指定的如果是文件夹,路径最后一定要以/结尾。指定多个文件或者文件夹时路径中间以英文逗号分隔。好了,知道了spring.config.location属性,我们就知道多环境配置文件外置的方法了。例如我想把所有配置文件application.properties、application-dev.properties和application-prod.properties放到项目文件夹下的Resources/config目录下,那么我的完整主类代码如下:package com.gitee.swsk33.test; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class TestApplication { public static void main(String[] args) { // 设定配置文件扫描目录 System.setProperty("spring.config.location", "file:Resources/config/"); SpringApplication.run(TestApplication.class, args); } }可见很简单,只在主方法main中最头部写了System.setProperty("spring.config.location", "file:Resources/config/");这一行代码即可完成配置。然后我的三个配置文件放在项目文件夹下的Resources/config目录下:再在主配置文件里面配置:spring.profiles.active=dev运行,即可使用开发环境的配置文件application-dev.properties了。5,配置文件改名我们也知道,Spring Boot配置文件默认名字是application.properties,Spring Boot默认情况下也是通过搜寻这个名字的文件找到配置文件的。如果说想改配置文件的名字怎么做呢?其实除了上述直接指定配置文件路径以外,还可以修改属性spring.config.name来实现,也是使用System.setProperty方法来修改。例如在main方法最前面写上:System.setProperty("spring.config.name", "config");那么Spring Boot就会去搜寻名为config.properties的文件作为配置文件。因此可见spring.config.name的默认值为application,这个值不需要写扩展名,扩展名会在Spring Boot中自动适配。上面修改了spring.config.name属性为config,那么如果说是多环境配置,我们的其余环境的配置文件也要跟着改为如下:config-dev.properties config-prod.properties6,总结Spring Boot的资源文件访问和我们普通Java程序可能有所不同,大家一定要注意资源文件配置,以及配置文件的加载。本文参考的官方文档:Spring Boot静态资源访问@PropertySource标识的使用Spring Boot外置配置文件
https会使我们的网站更加安全,起码看起来似乎好一些。这里分享Spring Boot配置https的步骤。1,去阿里云或者腾讯云等等申请SSL证书个人用户申请免费证书即可。阿里云免费证书申请方法然后在我们的控制台-SSL证书里面可以添加免费证书:然后点证书申请:根据其中指示填写完信息后,会让你给域名添加相应TXT记录,添加后即可申请。大约1-15分钟后证书申请完毕。然后在列表中点击下载按钮:下载jks格式:然后会得到个压缩包,里面有jks证书和密码。2,Spring Boot配置证书在Spring Boot配置文件application.properties中添加以下的配置:# SSL证书设置 server.ssl.key-store=证书jks文件所在位置 server.ssl.key-store-password=证书密码 server.ssl.keyStoreType=JKS根据自己的配置修改。建议一般证书文件放在项目文件夹\src\main\resources下。例如我的证书文件是ssl.jks放在项目文件夹\src\main\resources中,密码是123456,那么我的配置如下:# SSL证书设置 server.ssl.key-store=classpath:ssl.jks server.ssl.key-store-password=123456 server.ssl.keyStoreType=JKS路径需要说明的是,一般classpath:开头的表示jar包内路径,而在Spring Boot项目中项目文件夹\src\main\resources文件夹即可对应为classpath的根目录。当然也可以放在jar包外其余位置,例如放在项目文件夹中的ssl文件夹中,那么路径就以file:开头配置:server.ssl.key-store=file:ssl/ssl.jks这样就要最后保证生成的jar要和上述ssl文件夹放在同一目录,并保证运行目录就是jar所在目录。这样在开启项目,就是https了!3,配置http自动跳转https在启动类中加入如下代码:/** * http自动跳转https */ @Bean public ServletWebServerFactory servletContainer() { TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() { @Override protected void postProcessContext(Context context) { SecurityConstraint securityConstraint = new SecurityConstraint(); securityConstraint.setUserConstraint("CONFIDENTIAL"); SecurityCollection collection = new SecurityCollection(); collection.addPattern("/*"); securityConstraint.addCollection(collection); context.addConstraint(securityConstraint); } }; tomcat.addAdditionalTomcatConnectors(redirectConnector()); return tomcat; } private Connector redirectConnector() { Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol"); connector.setScheme("http"); connector.setPort(8801); // 原http端口 connector.setSecure(false); connector.setRedirectPort(8443); // 跳转的https端口,也就是我们配置文件中配置的项目端口 return connector; }注意这个Context类是org.apache.catalina包下的,Connector类是org.apache.catalina.connector包下的。根据自己的需要修改上述第二个方法中http端口和https端口。实际情况下,为了更方便地开启/关闭https,我们可以使用控制配置文件值实现动态注入Bean,以控制打开或者关闭https的功能。上述代码,servletContainer方法返回值会被注册为Bean,只有这个方法返回值注册为Bean了,才会开启https。因此我们使用@ConditionalOnProperty实现配置文件控制并动态注入。我们自定义一个配置名,这里就叫做swsk33.server.enablehttps,只有配置文件存在这一项配置且其值为true时,才会注入这个Bean,才会开启https,否则默认使用http。代码如下:@Bean @ConditionalOnProperty(name = {"swsk33.server.enablehttps"}, havingValue = "true") public ServletWebServerFactory servletContainer() { TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() { @Override protected void postProcessContext(Context context) { SecurityConstraint securityConstraint = new SecurityConstraint(); securityConstraint.setUserConstraint("CONFIDENTIAL"); SecurityCollection collection = new SecurityCollection(); collection.addPattern("/*"); securityConstraint.addCollection(collection); context.addConstraint(securityConstraint); } }; tomcat.addAdditionalTomcatConnectors(redirectConnector()); return tomcat; }重点是上面加了@ConditionalOnProperty注解,这个注解可以根据配置文件值,实现条件注入。其中name表示配置名,是个数组,havingValue表示指定配置必须要有这个值。也就是说必须当这个/多个指定配置名存在且为这个值时,才会注入这个Bean。最后在配置文件application.properties加入我们指定的配置:swsk33.server.enablehttps=true这样就开启了https,填false关闭https。注意关闭https的话还需要把上面讲到的以下关于证书配置项也给去掉:server.ssl.key-store server.ssl.key-store-password server.ssl.keyStoreType
Let's Encrypt是知名的免费SSL证书之一。这里分享一下今天研究的使用certbot工具生成免费ssl证书。1,连接服务器并安装certbot连接服务器,按照以下步骤安装certbot:(1) 删除旧的certbot软件包:sudo apt purge certbot sudo apt autoremove(2) 安装相关python环境sudo apt update sudo apt install python3 python3-venv libaugeas0(3) 为certbot创建虚拟python环境sudo python3 -m venv /opt/certbot/ sudo /opt/certbot/bin/pip install --upgrade pip(4) 安装certbotsudo /opt/certbot/bin/pip install certbot(5) 将certbot命令软链接到/usr/bin目录下sudo ln -s /opt/certbot/bin/certbot /usr/bin/certbot至此,certbot就安装配置完成了!输入certbot --help试试会输出帮助信息。2,开始手动生成证书说在前面的是,生成证书需要验证域名(一般是让你在域名解析商添加TXT记录),这里介绍两种域名验证方式:手动创建TXT记录验证(如果你直接使用的阿里云等等域名商的域名解析服务的话)【推荐】使用Cloudflare API的方式进行验证(如果你的域名是使用的cloudflare进行解析的话)下面我来一一说明这两种方式,大家根据实际情况选择任意一种即可。(1) 手动验证执行以下命令:sudo certbot certonly -d "你的域名" --manual --preferred-challenges dns-d参数表示域名,可以添加多个域名。还可以使用泛域名:sudo certbot certonly -d "你的域名" -d "*.你的域名" --manual --preferred-challenges dns --server https://acme-v02.api.letsencrypt.org/directory注意,使用泛域名的话,命令后面就多了--server https://acme-v02.api.letsencrypt.org/directory这个参数。如果要使用泛域名,建议看第二种使用cloudflare的验证方式,否则证书续期会发生错误例如我的域名是swsk33-web.link,那么生成证书的命令如下:sudo certbot certonly -d "swsk33-web.link" -d "*.swsk33-web.link" --manual --preferred-challenges dns --server https://acme-v02.api.letsencrypt.org/directory那么这个证书将会支持swsk33-web.link的访问,也支持xxx.swsk33-web.link的访问(xxx换成任意值)。然后提示你输入邮箱:输入你的邮箱,回车。然后提示你是否同意协议:输入y,回车。然后问你是否想收到其订阅邮件:这个随意,我这里选否,输入n回车。然后问你是否记录ip,这个必须选是,否则不会生成证书:输入y,回车。然后会让你给域名添加txt记录:去域名商登录并在域名解析里面按照要求添加TXT记录,然后稍等片刻按回车。最后,出现下图则成功:添加记录后,可能又会弹出一个让你添加TXT记录的消息,但这个记录名字和值不同,因此再添加这个记录到域名解析即可(也就是说,有时候你可能要添加两次TXT记录)证书生成在/etc/letsencrypt/live/你的域名目录下。这样,就完成了证书生成。(2) 使用Cloudflare API的验证方式由于我是在Dynadot上买的域名并且交给了Cloudflare进行解析,因此我采用第二种方式,使用Cloudflare API的验证非常方便,不需要手动添加TXT记录即可完成验证,并且很好地支持泛域名证书续期,推荐大家可以把域名放在Cloudflare进行解析然后使用这种方式。使用Cloudflare API方式验证,首先必须保证在Cloudflare上,域名已经解析到了你的服务器的ip地址上,否则会失败。第一次在服务器上生成证书时也会像上面第一种方式一样提示你输入邮箱、同意协议。首先,我们需要安装certbot-dns-cloudflare插件:sudo /opt/certbot/bin/pip install certbot-dns-cloudflare然后我们需要创建一个cloudflare配置文件,先在自己的Cloudflare账户中查看自己的Global API Key:将你自己的API Key复制下来保存。然后在服务器中新建一个配置文件cloudflare.ini,里面写入:dns_cloudflare_email = 你的cloudflare账户邮箱 dns_cloudflare_api_key = 你的API Key配置文件名和位置都可以自定义。然后开始生成证书:sudo certbot certonly -d "你的域名" --dns-cloudflare --dns-cloudflare-credentials "cloudflare配置文件路径" --dns-cloudflare-propagation-seconds 10如果要生成通配符证书,就如下:sudo certbot certonly -d "你的域名" -d "*.你的域名" --dns-cloudflare --dns-cloudflare-credentials "cloudflare配置文件路径" --dns-cloudflare-propagation-seconds 10 --server https://acme-v02.api.letsencrypt.org/directory上述--dns-cloudflare-propagation-seconds参数表示等待多少秒后开始验证,这里写10s即可。然后,你就发现证书直接成功生成了!证书同样生成在/etc/letsencrypt/live/你的域名目录下。3,将PEM证书转换为p12以配置进Spring Boot先通过下列命令转换证书:sudo openssl pkcs12 -export -in "你的证书文件路径" -inkey "你的私钥文件路径" -out "指定生成的p12证书文件路径"例如我这边:sudo openssl pkcs12 -export -in "/etc/letsencrypt/live/swsk33-web.link/cert.pem" -inkey "/etc/letsencrypt/live/swsk33-web.link/privkey.pem" -out "/etc/letsencrypt/live/swsk33-web.link/key.p12"执行命令会让你设定p12证书的密码,自行设定并记住,待会配置需要用。这样就生成了!然后打开你的Spring Boot项目的配置文件application.properties,添加如下配置:# SSL证书设置 server.ssl.key-store=file:刚刚生成的p12证书路径 server.ssl.key-store-password=刚刚生成p12证书时设定的p12证书密码 server.ssl.keyStoreType=PKCS12例如我的配置:# SSL证书设置 server.ssl.key-store=file:/etc/letsencrypt/live/swsk33-web.link/key.p12 server.ssl.key-store-password=123456 server.ssl.keyStoreType=PKCS12启动项目,即可发现你的网站变成https了!4,续期,吊销但是这个证书只有三个月有效期,我们需要大致在2个月后进行续期。通过以下命令续期,并重新转换PEM到P12证书:sudo certbot renew sudo openssl pkcs12 -export -in "你的证书文件路径" -inkey "你的私钥文件路径" -out "指定生成的p12证书文件路径"这里需要说明的是,续期后进行转换时,指定的p12文件路径和密码最好是和第一次生成的路径和密码一致,这样省的我们再去改Spring Boot配置。如果说证书不需要了,我们可以进行吊销。注意吊销不是删除证书就行了:sudo certbot revoke --cert-path "/etc/letsencrypt/live/你的证书名(一般就是域名)/cert.pem"可以通过certbot certificates命令查看自己的证书:例如我吊销证书swsk33-web.link:sudo certbot revoke --cert-path "/etc/letsencrypt/live/swsk33-web.link/cert.pem"按照提示输入y同意即可吊销。这个时候,/etc/letsencrypt/live/你的域名目录应该会被删除,如果发现没有删除建议手动删除它。后续出现问题可以手动删除/etc/letsencrypt再试。
2023年03月
2023年02月
2023年01月