Springboot后端实战篇

注册

image-20240614223823594

引入Spring Validation依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

导入Md5Util工具类

package com.zjf.utils;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class Md5Util {
/**
* 默认的密码字符串组合,用来将字节转换成 16 进制表示的字符,apache校验下载的文件的正确性用的就是默认的这个组合
*/
protected static char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

protected static MessageDigest messagedigest = null;

static {
try {
messagedigest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException nsaex) {
System.err.println(Md5Util.class.getName() + "初始化失败,MessageDigest不支持MD5Util。");
nsaex.printStackTrace();
}
}

/**
* 生成字符串的md5校验值
*
* @param s
* @return
*/
public static String getMD5String(String s) {
return getMD5String(s.getBytes());
}

/**
* 判断字符串的md5校验码是否与一个已知的md5码相匹配
*
* @param password 要校验的字符串
* @param md5PwdStr 已知的md5校验码
* @return
*/
public static boolean checkPassword(String password, String md5PwdStr) {
String s = getMD5String(password);
return s.equals(md5PwdStr);
}


public static String getMD5String(byte[] bytes) {
messagedigest.update(bytes);
return bufferToHex(messagedigest.digest());
}

private static String bufferToHex(byte bytes[]) {
return bufferToHex(bytes, 0, bytes.length);
}

private static String bufferToHex(byte bytes[], int m, int n) {
StringBuffer stringbuffer = new StringBuffer(2 * n);
int k = m + n;
for (int l = m; l < k; l++) {
appendHexPair(bytes[l], stringbuffer);
}
return stringbuffer.toString();
}

private static void appendHexPair(byte bt, StringBuffer stringbuffer) {
char c0 = hexDigits[(bt & 0xf0) >> 4];// 取字节中高 4 位的数字转换, >>>
// 为逻辑右移,将符号位一起右移,此处未发现两种符号有何不同
char c1 = hexDigits[bt & 0xf];// 取字节中低 4 位的数字转换
stringbuffer.append(c0);
stringbuffer.append(c1);
}

}

三层架构

UserController

package com.zjf.controller;
// 导入必要的包

@Validated// @Validated: 这个注解表示这个控制器中的方法参数将会进行校验。
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
UserService userservice;
// @Pattern: 这个注解用于校验方法参数是否符合正则表达式的规则。正则表达式"^\S{5,16}$",用户名和密码的长度为5~16位非空字符
@PostMapping("/register")
public Result register(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password) {
if (userService.findByUserName(username) != null) {
// 如果用户名已存在,返回错误信息
return Result.error("用户名已存在");
} else {
// 如果用户名不存在,调用userService的register方法注册新用户
userService.register(username, password);
// 注册成功,返回成功信息
return Result.success();
}
}
}

UserServiceImpl

package com.zjf.service.impl;

import com.zjf.utils.Md5Util;
// 导入必要的包

@Service
public class UserServiceImpl implements UserService {

@Autowired
UserMapper userMapper;
@Override
public void register(String username, String password) {
//// 使用Md5Util工具类对密码进行MD5加密
String md5String = Md5Util.getMD5String(password);
userMapper.insertUser(username,md5String);
}

@Override
public User findByUserName(String username) {
return userMapper.findByUserName(username);
}
}

UserMappper

package com.zjf.mapper;

@Mapper
public interface UserMapper {
@Insert("insert into user(username,password,create_time,update_time)" +
" values(#{username},#{password},now(),now())")
void insertUser(String username, String password);

@Select("select * from user where username=#{username}")
User findByUserName(String username);
}

知识要点

Md5加密

  1. 导入Md5Utils工具类

  2. 将输入的密码使用Md5Util类进行MD5加密生成md5String

    @Override
    public void register(String username, String password) {
    //加密
    String md5String = Md5Util.getMD5String(password);
    userMapper.insertUser(username,md5String);
    }

参数校验

使用 Spring Validation, 对注册接口的参数进行合法性校验

  1. 引入 Spring Validation 起步依赖

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
  2. 在参数前面添加 @Pattern 注解

    register(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password) 
  3. 在 Controller 类上添加 @Validated 注解

  4. 在全局异常处理器中处理参数校验失败的异常

    ​ 当用户名和密码的长度不满足5~16位非空字符时,参数校验失败,服务器抛出错误,“status”: 500, “error”: “Internal Server Error”,表示服务器遇到了一个内部错误。此时提供一个全局的异常处理机制。当应用程序中的任何控制器方法抛出异常时,handleException 方法会被调用。它会打印异常的堆栈跟踪信息,并返回一个包含错误信息的 Result 对象给客户端。这种方式有助于统一异常处理逻辑,提高代码的可维护性。

    package com.zjf.exception;
    //导入必要的包

    // @RestControllerAdvice: 这是一个Spring框架的注解,用于标记一个类作为全局的异常处理类。
    // 这个类会处理所有控制器(@Controller或@RestController)中抛出的异常,并将异常处理结果以JSON或其他序列化形式返回给客户端。
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    // @ExceptionHandler(Exception.class): 这个方法级别的注解用于指定这个方法将会处理Exception.class类型的异常。
    // 在这里,它指定了这个方法将处理所有的异常,因为Exception是所有异常类的基类。
    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e){
    e.printStackTrace(); // 打印异常的堆栈跟踪信息到控制台,这通常用于开发过程中的调试。
    return Result.error(StringUtils.hasLength(e.getMessage())? e.getMessage() : "操作失败");
    // 使用StringUtils.hasLength(e.getMessage())来检查异常是否有消息内容,如果有,则将异常消息作为错误信息返回。
    // 如果没有,则返回一个默认的字符串"操作失败"。
    // 这里的Result是一个自定义的响应实体类,它包含了处理异常后的结果。
    }
    }

登录

image-20240615090941613

引入jwt依赖

<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>

导入工具类jwtUtil

package com.zjf.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import java.util.Date;
import java.util.Map;

public class JwtUtil {

private static final String KEY = "itheima";

//接收业务数据,生成token并返回
public static String genToken(Map<String, Object> claims) {
return JWT.create()
.withClaim("claims", claims)
.withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 ))
.sign(Algorithm.HMAC256(KEY));
}

//接收token,验证token,并返回业务数据
public static Map<String, Object> parseToken(String token) {
return JWT.require(Algorithm.HMAC256(KEY))
.build()
.verify(token)
.getClaim("claims")
.asMap();
}

}

编写UserController

@PostMapping("/login")
public Result login(
@Pattern(regexp = "^\\S{5,16}$") String username,
@Pattern(regexp = "^\\S{5,6}$") String password) {
// 根据用户名查询用户信息
User loginUser = userService.findByUserName(username);
// 判断该用户是否存在
if (loginUser == null) {
// 如果用户不存在,返回错误信息
return Result.error("用户名错误");
}

// 判断密码是否正确,loginUser对象中的password是密文
if (Md5Util.getMD5String(password).equals(loginUser.getPassword())) {
// 如果密码正确,登录成功
// 创建一个包含用户ID和用户名的声明映射
Map<String, Object> claims = new HashMap<>();
claims.put("id", loginUser.getId());
claims.put("username", loginUser.getUsername());
// 使用JwtUtil工具类生成一个JWT令牌,将声明映射作为载荷
String token = JwtUtil.genToken(claims);
// 返回成功结果,包含生成的JWT令牌
return Result.success(token);
}
// 如果密码错误,返回错误信息
return Result.error("密码错误");
}

登录拦截器

LoginInterceptor

LoginInterceptor是一个用于验证用户是否登录的拦截器。它通过检查HTTP请求中的JWT来验证用户的身份,如果JWT有效,则允许请求继续;如果JWT无效,则拒绝请求。

package com.zjf.interceptors;

@Component
public class LoginInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
try {
Map<String, Object> claims = JwtUtil.parseToken(token);
return true; // 如果令牌验证成功,返回true,继续执行后续的请求处理
} catch (Exception e) {
// 如果解析令牌时出现异常,说明令牌可能无效或过期
// 设置响应状态码为401,
response.setStatus(401);
return false;
}
}
}

WebConfig

WebConfig是一个配置类,用于注册一个登录拦截器(LoginInterceptor)。通过实现Spring MVC的WebMvcConfigurer接口,并重写addInterceptors方法,将登录拦截器添加到拦截器注册表中。在拦截器的配置中,指定了不拦截登录接口和注册接口的路径,即对路径为/user/login和/user/register的请求不进行拦截

package com.zjf.config;

import com.zjf.interceptors.LoginInterceptor;
import com.zjf.interceptors.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Autowired
private LoginInterceptor loginInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
//登录接口和注册接口不拦截
registry.addInterceptor(loginInterceptor).excludePathPatterns("/user/login","/user/register");
}
}

知识要点

JWT

JSON Web Token(JWT)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为 JSON 对象。JWT 可以通过数字签名进行验证,因此是一种非常安全的方式,通常用于在身份验证(Authentication)和信息交换(Information Exchange)方面。

JWT 结构

JWT 由三部分组成,通过点(.)分隔:

image-20240615112525158

  1. Header(头部): 包含了令牌的元数据信息,例如算法(HS256、RS256、HMAC256)、令牌类型(JWT)等。
  2. Payload(负载): 包含了被加密的数据,例如用户的身份信息或其他声明。
  3. Signature(签名): 使用头部指定的算法和密钥对头部和负载进行签名,确保数据的完整性和认证。

这三部分通过Base64 URL编码后连接在一起,形成最终的JWT令牌。

JWT 使用

JWT 的主要使用场景是在客户端和服务器之间安全地传递信息,特别是在身份验证过程中。一般的流程如下:

  1. 认证流程:

    • 用户向服务器提供用户名和密码进行身份验证。
    • 服务器验证用户的身份,并生成包含用户信息的JWT。
    • 服务器将生成的JWT返回给客户端。
  2. 请求认证:

    • 客户端在以后的请求中将JWT放置在请求头部的Authorization字段中,通常格式为 Authorization: Bearer <token>

    • 服务器接收到请求后,解析JWT并验证签名的有效性。

    • 如果验证成功,服务器处理请求;如果验证失败或者JWT过期,服务器拒绝请求。


拦截器

拦截器(Interceptor)是一种动态拦截方法调用的机制,在SpringMVC中动态拦截控制器方法的执行,是java中AOP思想的运用。

  1. 创建拦截器

在Spring框架中,拦截器是通过实现HandlerInterceptor接口来创建的。这个接口定义了三个方法,分别对应请求处理的不同阶段:

  • preHandle(HttpServletRequest request, HttpServletResponse response, Object handler):这个方法在请求实际处理之前被调用。在这个方法中,可以执行一些前置处理,如身份验证、权限检查等。如果这个方法返回true,则请求会继续执行;如果返回false,则请求将被终止。

  • postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView):这个方法在请求实际处理之后,但在视图渲染之前被调用。在这个方法中,可以对模型的属性进行修改,或者添加额外的模型数据。

  • afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex):这个方法在整个请求完成后被调用,即在视图渲染之后。在这个方法中,可以执行一些清理工作,如释放资源、记录日志等。

  1. 配置拦截器

    创建一个配置类,实现WebMvcConfigurer接口,并重写addInterceptors方法。在这个方法中,你可以使用InterceptorRegistry来注册你的拦截器,并指定拦截器应该作用的路径或者排除的路径。

  2. 注册拦截器

    addInterceptors方法中,使用InterceptorRegistryaddInterceptor方法来注册你的拦截器。然后,你可以使用addPathPatterns来指定拦截器应该拦截哪些路径,或者使用excludePathPatterns来指定拦截器不应该拦截哪些路径。

获取用户详细信息

image-20240617151915415

编写UserController

注意:/userInfo接口不携带参数username,token令牌中存放了用户的id和usern,通过携带token令牌,获取username。@RequestHeader注解用于将请求头中的信息绑定到控制器方法参数上。当你在方法参数前面加上@RequestHeader注解,并指定name属性时,Spring会自动将请求中名为Authorization的头部的值赋给该参数。当HTTP请求到达服务器并调用这个方法时,Spring会查找请求头中名为Authorization的字段,并将其值作为字符串参数authorizationHeader传递给方法。

@GetMapping("/userInfo")
public Result<User> userInfo(@RequestHeader(name = "Authorization") String token){
// 使用JwtUtil工具类来解析传递的JWT字符串,获取其中的负载(Payload)
Map<String, Object> map = JwtUtil.parseToken(token);
// 从解析出来的负载中获取用户的用户名
String username =(String) map.get("username");
// 使用userService的findByUserName方法根据用户名查询用户的信息
User user = userService.findByUserName(username);
// 如果查询到用户,则将用户信息封装在Result对象中返回,表示请求成功
return Result.success(user);
}

测试结果

image-20240617154357256

​ 在测试结果中,用户的密码password同样也响应给客户端。密码通常是敏感信息,不应该在网络请求的响应中返回给客户端。这可能导致用户隐私泄露的问题。在设计和实现时,应实现将密码等敏感信息从序列化的过程中排除出去。

@JsonIgnore是Jackson库中的一个注解,它用于在将Java对象序列化为JSON格式时忽略特定的属性。当你想要隐藏某些敏感信息或不希望这些信息被客户端看到时,比如密码、加密密钥等。在Java类的某个字段或方法上添加@JsonIgnore注解,Jackson在序列化过程中就会跳过这个字段或方法,不会将其包含在生成的JSON中。

​ 在本例中User类的password字段上加上**@JsonIgnore**,实现在被序列化成 JSON 时,password 字段将会被忽略,不会出现在生成的 JSON 中。

@JsonIgnore
private String password;//密码

测试结果

image-20240617155435964

ThreadLocal

ThreadLocal,即线程本地变量,提供线程局部变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是在操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了并发场景下的线程安全问题

  • 用来存取数据 : set()/get()
  • 使用 ThreadLocal 存储的数据 , 线程安全
  • 用完记得调用 remove 方法释放

image-20240617160823649

​ 在之前的代码中,我们使用**@RequestHeader**注解获取请求头中名为Authorization的头部的值,并使用JwtUtil工具类来解析token,获取其中的username。如果存在多个这样的接口,相应的会重复编写这串代码。通过拦截器中通一的解析token实现代码的复用

导入ThreadLocal 工具类

package com.zjf.utils;

import java.util.HashMap;
import java.util.Map;

/**
* ThreadLocal 工具类
*/
@SuppressWarnings("all")
public class ThreadLocalUtil {
//提供ThreadLocal对象,
private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();

//根据键获取值
public static <T> T get(){
return (T) THREAD_LOCAL.get();
}

//存储键值对
public static void set(Object value){
THREAD_LOCAL.set(value);
}


//清除ThreadLocal 防止内存泄漏
public static void remove(){
THREAD_LOCAL.remove();
}
}

在登录拦截器中实现线程局部变量

@Component
public class LoginInterceptor implements HandlerInterceptor{
// preHandle方法在请求处理之前调用(即在Controller方法之前)
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
try {
// 使用JwtUtil工具类解析令牌,获取其中的负载
Map<String, Object> claims = JwtUtil.parseToken(token);
// 将解析出的负载存储到线程局部变量中,以便在后续的请求处理中可以使用
ThreadLocalUtil.set(claims);
// 如果令牌验证成功,返回true,继续执行后续的请求处理
return true;
} catch (Exception e) {
// 如果解析令牌时出现异常,说明令牌可能无效或过期
// 设置响应状态码为401
response.setStatus(401);
// 返回false,不再继续执行后续的请求处理
return false;
}
}
// afterCompletion方法在请求处理完成后调用(即在Controller方法之后)
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清理线程局部变量,避免内存泄漏
ThreadLocalUtil.remove();
}
}

修改/userInfo接口

@GetMapping("/userInfo")
public Result<User> userInfo(){
Map<String, Object> map = ThreadLocalUtil.get(); // 使用ThreadLocalUtil工具类从线程局部变量中获取存储的数据
String username = (String) map.get("username"); // 从获取的数据中提取用户名
User user = userService.findByUserName(username); // 使用findByUserName方法根据用户名查询用户的信息
return Result.success(user);
}

更新用户基本信息

image-20240617195754402

三层架构

UserController

@PutMapping("/update")
public Result update(@RequestBody User user) {
userService.update(user);
return Result.success();
}

UserServiceImpl

@Override
public void update(User user) {
user.setUpdateTime(LocalDateTime.now());
userMapper.update(user);
}

UserMapper

@Update("update user set nickname = #{nickname},email = #{email},update_time = #{updateTime} where id = #{id}")
void update(User user);

参数校验

image-20240617200259478

对上述的更新用户基本信息update代码进行参数校验,不同与之前注册时对属性的参数,update方法传入的参数是一个User对象

image-20240617203311920

实体参数校验

  1. 实体类的成员变量上添加注解
    • @NotNull
    • @NotEmpty
    • @Email
  2. 接口方法的实体参数上添加 @Validated 注解

更新用户头像

image-20240618110700127

三层架构

UserController

@PatchMapping("/updateAvatar")
public Result updateAvatar(@RequestParam @URL String avatarUrl){
userService.updateAvatar(avatarUrl);
return Result.success();
}

UserServiceImpl

@Override
public void updateAvatar(String avatarUrl) {
Map<String, Object> map = ThreadLocalUtil.get();//ThreadLocalUtil中获取一个Map对象
Integer id = (Integer) map.get("id");//从Map对象中获取用户id
userMapper.updateAvatar(avatarUrl,id);
}

UserMapper

@Update("update user set user_pic = #{userPic},update_time = NOW() where id = #{id}")//update_time字段更新为当前时间,使NOW()函数获取
void updateAvatar(String userPic,Integer id);

知识要点

参数校验

@URL注解,通常用于参数验证,表示该参数必须是一个合法的URL格式。

public Result updateAvatar(@RequestParam @URL String avatarUrl)

@PatchMapping

@PatchMapping 注解用于将 HTTP PATCH 请求映射到指定的处理方法。HTTP PATCH 方法通常用于更新资源的部分内容,而不是替换整个资源。@PatchMapping 用于部分更新资源,而 @PutMapping 用于替换整个资源。@PatchMapping 请求仅包含要更新的部分内容,而 @PutMapping 请求包含完整的资源表述。

更新用户密码

image-20240618154824407

三层架构

Usercontroller

@PatchMapping("/updatePwd")
public Result updatePwd(@RequestBody Map<String,String> params){
// 从请求体中获取旧密码、新密码和确认密码。
String oldPwd = params.get("old_pwd");
String newPwd = params.get("new_pwd");
String re_pwd = params.get("re_pwd");
// 判断这三个密码参数是否为空,如果为空,返回错误信息。
if(!StringUtils.hasLength(oldPwd)||!StringUtils.hasLength(newPwd)||!StringUtils.hasLength(re_pwd)){
return Result.error("参数错误");
}
// 从ThreadLocal中获取用户名,这通常在用户登录时设置。
Map<String,Object> map= ThreadLocalUtil.get();
String username = (String) map.get("username");
// 使用用户名查询当前登录的用户信息。
User loginUser = userService.findByUserName(username);
// 比较传入的旧密码的MD5值是否与数据库中的一致,不一致则返回错误信息。
if(!Md5Util.getMD5String(oldPwd).equals(loginUser.getPassword())){
return Result.error("旧密码错误");
}
// 检查新密码是否与旧密码相同,如果相同则返回错误信息。
if(Md5Util.getMD5String(newPwd).equals(loginUser.getPassword())){
return Result.error("新密码不能与旧密码一致");
}
// 检查新密码和确认密码是否相同,不同则返回错误信息。
if(!newPwd.equals(re_pwd)){
return Result.error("两次密码不一致");
}
// 如果所有检查都通过,调用userService的updatePwd方法更新密码。
userService.updatePwd(newPwd);
return Result.success();
}

UserService

@Override
public void updatePwd(String newPwd) {
Map<String, Object> map = ThreadLocalUtil.get();
Integer id = (Integer) map.get("id");
String md5String = Md5Util.getMD5String(newPwd);//对新密码进行MD5加密
userMapper.updatePwd(md5String,id);
}

UserMapper

@Update("update user set password = #{newPwd},update_time = now() where id = #{id}")
void updatePwd(String newPwd, Integer id);

新增文章分类

image-20240619091431586

三层架构

CategoryController

package com.zjf.controller;

@RestController
@RequestMapping("/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;

@PostMapping
public Result add(@RequestBody @Validated Category category){
categoryService.add(category);
return Result.success();
}
}

CategoryServiceImpl

package com.zjf.service.impl;

@Service
public class CategoryServiceImpl implements CategoryService {

@Autowired
private CategoryMapper categoryMapper;
@Override
public void add(Category category) {
Map<String,Object> map = ThreadLocalUtil.get();
Integer id = (Integer) map.get("id");//获取当前用户id值
category.setCreateTime(LocalDateTime.now());
category.setUpdateTime(LocalDateTime.now());
category.setCreateUser(id);//将当前用户id值赋给创建分类用户Create_User字段
categoryMapper.add(category);
}
}

CategoryMapper

@Mapper
public interface CategoryMapper {
@Insert("insert into category(category_name, category_alias, create_user, create_time, update_time)" +
" VALUE(#{categoryName},#{categoryAlias},#{createUser},#{createTime},#{updateTime})")
void add(Category category);
}

知识要点

参数校验

未进行控制层的参数校验之前,测试时如果传入categoryName或categoryName为空,系统会在SQL层抛出错误,通过参数校验,实现controller层抛出错误

1.在Category实体类 categoryName, categoryName字段上添加@NotEmpty注解

@NotEmpty
private String categoryName;//分类名称
@NotEmpty
private String categoryName;//分类别名

2.在控制层对应方法参数前加上@Validated注解

public Result add(@RequestBody @Validated Category category)

文章分类列表

image-20240619093838445

三层架构

CategoryController

@GetMapping
public Result<List<Category>> list(){
List<Category> cs = categoryService.list();
return Result.success(cs);
}

CategoryServiceImpl

public List<Category> list() {
Map<String,Object> map = ThreadLocalUtil.get();
Integer id = (Integer) map.get("id");//获取当前用户id
return categoryMapper.list(id);
}

CategoryMapper

@Select("select * from category where create_user = #{id}")
List<Category> list(Integer id);//id是user表id,也就是category表的create_user

知识要点

@JsonFormat注解

用于格式化Java对象中的日期和时间属性,当这个对象被转换为JSON字符串时,会按照指定的格式来格式化日期和时间

private LocalDateTime createTime;//创建时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;//更新时间

测试结果:

{
"id": 1,
"categoryName": "美食",
"categoryAlias": "ms",
"createUser": 10,
"createTime": "2024-06-18T17:29:26",//未使用注解
"updateTime": "2024-06-18 17:29:26"//使用了@JsonFrmat注解
}

获取文章分类详情

image-20240619100801212

三层架构

CategoryController

@GetMapping("/detail")
public Result<Category> detail(Integer id){//参数id使category表的id
Category category = categoryService.detail(id);
return Result.success(category);//返会category对象
}

CategoryServiceImpl

@Override
public Category detail(Integer id) {
return categoryMapper.findById(id);
}

CategoryMapper

@Select("select * from category where id = #{id}")
Category findById(Integer id);

更新文章分类

image-20240619102814168

三层架构

CategoryController

@PutMapping
public Result update(@RequestBody @Validated(Category.Update.class) Category category){//@Validated参数校验
categoryService.update(category);
return Result.success();

CategoryServiceImpl

@Override
public void update(Category category) {
category.setUpdateTime(LocalDateTime.now());
categoryMapper.update(category);
}

CategoryMapper

@Update("update category set category_name=#{categoryName},category_alias=#{categoryAlias},update_time=#{updateTime} where id=#{id}")
void update(Category category);

知识要点

参数校验

更新文章分类参数说明:

参数名称 说明 类型 是否必须 备注
id 主键ID number
categoryName 分类名称 string
categoryAlias 分类别名 string

id、categoryName、categoryAlias均为必须,故在实体类Category类的相关字段上添加相应注解

@NotNull
private Integer id;//主键ID
@NotEmpty
private String categoryName;//分类名称
@NotEmpty
private String categoryAlias;//分类别名

注意:在新增文章分类中,已经为categoryName、categoryAlias添加了@NotEmpty注解。通过给id、categoryName、categoryAlias添加注解,以及为控制层方法参数添加@Validated注解,实现了更新文章分类的参数校验,但我们注意到在新增文章分类方法中,也为相同的参数category添加了@Validated参数校验。此时,在执行新增文章分类时,会抛出id 不能为null的错误。为解决此问题,就要用到分组校验。

分组校验

​ 把校验项进行归类分组,在完成不同的功能的时候,校验指定组中的校验项

  1. 定义分组

    在category实体类中定义了两个空接口Add和Update,用于分组校验

    public class Category {
    public interface Add{
    }
    public interface Update{
    }
    }
  2. 定义校验项时指定归属的分组

    @NotNull(groups = Update.class)
    private Integer id;//主键ID
    @NotEmpty(groups = {Add.class, Update.class})
    private String categoryName;//分类名称
    @NotEmpty(groups = {Add.class, Update.class})
    private String categoryAlias;//分类别名
  3. 校验时指定要校验的分组

    public Result update(@RequestBody @Validated(Category.Update.class) Category category){
    }
public Result add(@RequestBody @Validated(Category.Add.class) Category category){
}

注:

  • 定义校验时,如果没有指定分组,默认属于default组

  • 分组之间可以继承,A extends B,那么A用有B中所有的校验项

    上述步骤2.定义校验项时指定归属的分组,可简化如下

    @NotNull(groups = Update.class)
    private Integer id;//主键ID
    @NotEmpty
    private String categoryName;//分类名称
    @NotEmpty
    private String categoryAlias;//分类别名

删除文章分类

三层架构

CategoryController

@DeleteMapping
public Result delete(Integer id){
categoryService.delete(id);
return Result.success();
}

CategoryServiceImpl

@Override
public void delete(Integer id) {
categoryMapper.delete(id);
}

CategoryMapper

@Delete("delete from category where id=#{id}")
void delete(Integer id);

新增文章

image-20240619161039625

三层架构

ArticleController

@RestController
@RequestMapping("/article")
public class ArticleController {

@Autowired
private ArticleService articleService;;

@PostMapping
public Result add(@RequestBody Article article){
articleService.add(article);
return Result.success();
}
}

ArticleServiceImpl

@Service
public class ArticleServiceImpl implements ArticleService {

@Autowired
private ArticleMapper articleMapper;
@Override
public void add(Article article) {
Map<String,Object> map = ThreadLocalUtil.get();
Integer id = (Integer) map.get("id");
article.setCreateUser(id);
article.setCreateTime(LocalDateTime.now());
article.setUpdateTime(LocalDateTime.now());
articleMapper.add(article);
}
}

ArticleMapper

@Mapper
public interface ArticleMapper {
@Insert("insert into article(title,content,cover_img,state,category_id,create_user,create_time,update_time)" +"" +
"values (#{title},#{content},#{coverImg},#{state},#{categoryId},#{createUser},#{createTime},#{updateTime})")
void add(Article article);
}

知识要点

参数校验

新增文章请求参数说明:

参数名称 说明 类型 是否必须 备注
title 文章标题 string 1~10个非空字符
content 文章正文 string
coverImg 封面图像地址 string 必须是url地址
state 发布状态 string 已发布 | 草稿
categoryId 文章分类ID number

自定义参数校验
已有的注解不能满足所有的校验需求,特殊的情况需要自定义校验 ( 自定义校验注解 )

  1. 自定义注解 State

    package com.zjf.anno;

    @Documented//元注解
    @Target({FIELD})//元注解
    @Retention(RUNTIME)//元注解
    @Constraint(validatedBy = {StateVlidation.class})
    public @interface State {
    //提供校验后的提示信息
    String message() default "{state参数的值只能是已发布或草稿}";
    //指定分组
    Class<?>[] groups() default {};
    //负载 获取到State注解的附加信息
    Class<? extends Payload>[] payload() default {};
    }
  2. 自定义校验数据的类 StateValidation实现 ConstraintValidator 接口

    package com.zjf.validation;

    public class StateVlidation implements ConstraintValidator<State,String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
    if(value==null){
    return false;
    }
    if(value.equals("已发布")||value.equals("草稿")){
    return true;
    }
    return false;
    }
    }
  3. 在需要校验的地方使用自定义注解

public class Article {
private Integer id;//主键ID
@NotEmpty
@Pattern(regexp = "^\\S{1,10}$")
private String title;//文章标题
@NotEmpty
private String content;//文章内容
@NotEmpty
@URL
private String coverImg;//封面图像

@State
private String state;//发布状态 已发布|草稿
@NotNull
private Integer categoryId;//文章分类id
private Integer createUser;//创建人ID
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//更新时间
}

文章列表 ( 条件分页 )

引入pagehelper依赖

<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>

导入PageBean实体

package com.zjf.pojo;

//分页返回结果对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageBean <T>{
private Long total;//总条数
private List<T> items;//当前页数据集合
}

三层架构

ArticleController

@GetMapping
// pageNum参数用于接收请求的页码,pageSize参数用于接收每页显示的记录数;
//@RequestParam(required = false)表示该参数是可选的,可输入也可不输入
public Result<PageBean<Article> > list(Integer pageNum, Integer pageSize, @RequestParam(required = false) Integer categoryId, @RequestParam(required = false) String state){
PageBean<Article> pageBean = articleService.list(pageNum,pageSize,categoryId,state); // 获取分页查询的结果,并存储在pageBean对象中
return Result.success(pageBean);
}

ArticleServiceImpl

/**
* 重写list方法,以实现分页查询文章列表的功能。
* 根据传入的页码、每页记录数、分类ID和文章状态来筛选和分页文章。
*
* @param pageNum 请求的页码
* @param pageSize 每页显示的记录数
* @param categoryId 文章分类ID
* @param state 文章的状态
* @return 包含分页信息和文章列表的PageBean对象
*/
@Override
public PageBean<Article> list(Integer pageNum, Integer pageSize, Integer categoryId, String state) {
PageBean<Article> pb = new PageBean<>(); // 创建PageBean对象,用于存储分页信息和文章列表
PageHelper.startPage(pageNum, pageSize); //开启分页查询
Map<String, Object> map = ThreadLocalUtil.get();
Integer id = (Integer) map.get("id");//获取用户当前id
List<Article> as = articleMapper.list(categoryId, state, id);
// 将查询结果转换为Page对象
Page<Article> p = (Page<Article>) as;
// 设置PageBean的总记录数,从Page对象中获取
pb.setTotal(p.getTotal());
// 设置PageBean的文章列表,从Page对象中获取查询结果
pb.setItems(p.getResult());
// 返回包含分页信息和文章列表的PageBean对象
return pb;
}

ArticleMapper

List<Article> list(Integer categoryId, String state, Integer id);

ArticleMapper.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.zjf.mapper.ArticleMapper">
<select id="list" resultType="com.zjf.pojo.Article">
select * from article
<where>
<if test="categoryId!=null">
category_id=#{categoryId}
</if>
<if test="state!=null and state!=''">
and state=#{state}
</if>
and create_user=#{id}
</where>
</select>
</mapper>

知识要点

@RequestParam

@RequestParam注解是将请求参数绑定到控制器的方法参数上

1、语法:@RequestParam(value=”参数名”,required=”true/false”,defaultValue=””)

2、属性:

  • value:表示参数名,即前端页面传过来的参数名
  • defaultValue:参数默认值,如果设置了该值,required=true将失效,自动为false,如果没有传该参数,就使用默认值
  • required:表示是否要强制包含该参数,默认值为false,表示允许请求中不包含该参数,并且该参数值会为设为null。true表示该请求中必须包含该参数否则报错

动态SQL

动态 SQL_MyBatis中文网

获取文章详情

三层架构

ArticleController

@GetMapping("/detail")
public Result detail(Integer id){
Article article = articleService.findById(id);
return Result.success(article);
}

ArticleServiceImpl

@Override
public Article findById(Integer id) {
Article article = articleMapper.findById(id);
return article;
}

ArticleMapper

@Select("select * from article where id=#{id}")
Article findById(Integer id);

参数校验

响应参数说明:

名称 类型 是否必须 默认值 备注 其他信息
code number 必须 响应码, 0-成功,1-失败
message string 非必须 提示信息
data object 必须 返回的数据
|-id number 非必须 主键ID
|-title string 非必须 文章标题
|-content string 非必须 文章正文
|-coverImg string 非必须 文章封面图像地址
|-state string 非必须 发布状态 已发布|草稿
|-categoryId number 非必须 文章分类ID
|-createTime string 非必须 创建时间
|-updateTime string 非必须 更新时间

响应数据样例:

{
"code": 0,
"message": "操作成功",
"data": {
"id": 4,
"title": "北京旅游攻略",
"content": "天安门,颐和园,鸟巢,长城...爱去哪去哪...",
"coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png",
"state": "已发布",
"categoryId": 2,
"createTime": "2023-09-03 11:35:04",
"updateTime": "2023-09-03 11:40:31"
}
}

使用@JsonIgnore 注解在序列化和反序列化时忽略createUser字段;使用@JsonFormat(pattern = “yyyy-MM-dd HH:mm:ss”) 注解在序列化和反序列化时,按照指定的日期时间格式进行处理。

@JsonIgnore
private Integer createUser;//创建人ID
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;//创建时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;//更新时间

更新文章

三层架构

ArticleController

@PutMapping
public Result update(@RequestBody @Validated(Category.Update.class) Category category){
categoryService.update(category);
return Result.success();
}

ArticleServiceImpl

@Override
public void update(Category category) {
category.setUpdateTime(LocalDateTime.now());
categoryMapper.update(category);
}

ArticleMapper

@Update("update article set title=#{title},content=#{content},cover_img=#{coverImg},state=#{state},category_id=#{categoryId},update_time=now() where id=#{id}")
void update(Article article);

参数校验

请求参数说明:

参数名称 说明 类型 是否必须 备注
id 主键ID number
title 文章标题 string
content 文章正文 string
coverImg 封面图像地址 string
state 发布状态 string 已发布 | 草稿
categoryId 文章分类ID number

分组校验:

public class Article {
@NotNull(groups = Article.update.class)
private Integer id;//主键ID
//新增文章已添加,保持不变
public interface add{

}
public interface update{

}
}
@PostMapping
public Result add(@RequestBody @Validated(Article.add.class) Article article){

}
@PutMapping
public Result update(@RequestBody @Validated(Article.update.class) Article article){

}

删除文章

三层架构

ArticleController

@DeleteMapping
public Result delete(Integer id){
articleService.delete(id);
return Result.success();
}

ArticleServiceImpl

@Override
public void delete(Integer id) {
articleMapper.delete(id);
}

ArticleMapper

@Delete("delete from article where id=#{id}")
void delete(Integer id);

文件上传

本地存储

FileUploadController

package com.zjf.controller;

@RestController
public class FileUploadController {
@PostMapping("/upload")
public Result<String> upload(MultipartFile file) throws Exception {
// 获取上传文件的原始文件名
String originalFilename = file.getOriginalFilename();
// 生成一个新的文件名,防止文件名冲突
// 使用UUID生成唯一标识,并保留原始文件的扩展名
String filename = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));

// 将文件保存到本地文件系统的代码
file.transferTo(new File("D:\\桌面\\大事件\\files\\" + filename));
// 返回一个包含文件URL的Result对象,表示文件上传成功
return Result.success("url地址......");
}
}

阿里云 OSS

阿里云对象存储 OSS ( Object Storage Service ),是一款海量、安全、低成本、高可靠的云存储服务。使用 OSS ,您
可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。

image-20240621161522818


实现步骤

  1. 引入SDK

    <dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.15.1</version>
    </dependency>
  2. 导入 AliOssUtil工具类

    package com.zjf.bigeventadmin.utils;

    import com.aliyun.oss.ClientException;
    import com.aliyun.oss.OSS;
    import com.aliyun.oss.OSSClientBuilder;
    import com.aliyun.oss.OSSException;

    import java.io.InputStream;

    public class AliOssUtil {
    private static final String ENDPOINT = "https://oss-cn-beijing.aliyuncs.com";
    private static final String ACCESS_KEY_ID = "LTAI5tQ8e13igWZUMTjMEEQV";
    private static final String SECRET_ACCESS_KEY = "MffMJoM24sc59SEBEJQDb0cfBVOAC9";
    private static final String BUCKET_NAME = "big-event-gwd";

    //上传文件,返回文件的公网访问地址
    public static String uploadFile(String objectName, InputStream inputStream){
    // 创建OSSClient实例。
    OSS ossClient = new OSSClientBuilder().build(ENDPOINT,ACCESS_KEY_ID,SECRET_ACCESS_KEY);
    //公文访问地址
    String url = "";
    try {
    // 创建存储空间。
    ossClient.createBucket(BUCKET_NAME);
    ossClient.putObject(BUCKET_NAME, objectName, inputStream);
    url = "https://"+BUCKET_NAME+"."+ENDPOINT.substring(ENDPOINT.lastIndexOf("/")+1)+"/"+objectName;
    } catch (OSSException oe) {
    System.out.println("Caught an OSSException, which means your request made it to OSS, "
    + "but was rejected with an error response for some reason.");
    System.out.println("Error Message:" + oe.getErrorMessage());
    System.out.println("Error Code:" + oe.getErrorCode());
    System.out.println("Request ID:" + oe.getRequestId());
    System.out.println("Host ID:" + oe.getHostId());
    } catch (ClientException ce) {
    System.out.println("Caught an ClientException, which means the client encountered "
    + "a serious internal problem while trying to communicate with OSS, "
    + "such as not being able to access the network.");
    System.out.println("Error Message:" + ce.getMessage());
    } finally {
    if (ossClient != null) {
    ossClient.shutdown();
    }
    }
    return url;
    }
    }

注意: ENDPOINT ,ACCESS_KEY_ID,ACCESS_KEY_SECRET,BUCKET_NAME根据个人对象存储OSS进行修改

  1. FileUploadController

    @PostMapping("/upload")
    public Result<String> upload(MultipartFile file) throws Exception {
    String originalFilename = file.getOriginalFilename();
    String filename = UUID.randomUUID().toString()+originalFilename.substring(originalFilename.lastIndexOf("."));
    String url = AliOssUtil.uploadFile(filename, file.getInputStream());
    return Result.success(url);
    }

登陆优化-redis

​ 在原来的登陆实现中,用户登陆系统,当修改密码成功后,那将来用户需要使用新的密码重新登陆系统。重新登录成功后,后台会下发新的令牌,但旧的令牌在之前的程序中没有作废,仍然可以使用旧的令牌访问用户资源。

SpringBoot 集成 redis

  • 导入 spring-boot-starter-data-redis 起步依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 在 yml 配置文件中 , 配置 redis 连接信息
spring:
data:
redis:
host: localhost
port: 6379
  • 调用 API(StringRedisTemplate) 完成字符串的存取操作

​ 编写redis测试代码

package com.zjf;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import java.util.concurrent.TimeUnit;

@SpringBootTest
public class RedisTest {
@Autowired
private StringRedisTemplate stringRedisTemplate;

@Test
public void test1()
{
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
operations.set("username", "zjf");
operations.set("id","1",15, TimeUnit.SECONDS);
}
@Test
public void test2()
{
String username = stringRedisTemplate.opsForValue().get("username");
System.out.println(username);
}
}

令牌主动失效机制

  • 登录成功后,给浏览器响应令牌的同时,把该令牌存储到 redis 中
/省略无关代码,保持不变/

@Autowired
private StringRedisTemplate stringRedisTemplate;

@PostMapping("/login")
public Result login(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password) {
User loginUser = userService.findByUserName(username);
//判断该用户是否存在
if (loginUser == null) {
return Result.error("用户名错误");
}

//判断密码是否正确 loginUser对象中的password是密文
if (Md5Util.getMD5String(password).equals(loginUser.getPassword())) {
//登录成功
Map<String, Object> claims = new HashMap<>();
claims.put("id", loginUser.getId());
claims.put("username", loginUser.getUsername());
String token = JwtUtil.genToken(claims);
// 使用stringRedisTemplate操作Redis,将token作为key,token本身作为value存入Redis
// 设置过期时间为1小时
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
operations.set(token, token, 1, TimeUnit.HOURS);
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
operations.set(token, token, 1, TimeUnit.HOURS);
return Result.success(token);
}
return Result.error("密码错误");
}
  • LoginInterceptor 拦截器中,需要验证浏览器携带的令牌,并同时需要获取到 redis 中存储的与之相同的令牌
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
try {
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
String redisToken = operations.get(token);
if (redisToken == null) {
throw new RuntimeException();
}

Map<String, Object> claims = JwtUtil.parseToken(token);
ThreadLocalUtil.set(claims);
return true;
} catch (Exception e) {
response.setStatus(401);
return false;
}
}
  • 当用户修改密码成功后,删除 redis 中存储的旧令牌
@PatchMapping("/updatePwd")
public Result updatePwd(@RequestBody Map<String,String> params,@RequestHeader("Authorization") String token){
String oldPwd = params.get("old_pwd");
String newPwd = params.get("new_pwd");
String re_pwd = params.get("re_pwd");
if(!StringUtils.hasLength(oldPwd)||!StringUtils.hasLength(newPwd)||!StringUtils.hasLength(re_pwd)){
return Result.error("参数错误");
}
Map<String,Object> map= ThreadLocalUtil.get();
String username = (String) map.get("username");
User loginUser = userService.findByUserName(username);
if(!Md5Util.getMD5String(oldPwd).equals(loginUser.getPassword())){
return Result.error("旧密码错误");
}
if(Md5Util.getMD5String(newPwd).equals(loginUser.getPassword())){
return Result.error("新密码不能与旧密码一致");
}
if(!newPwd.equals(re_pwd)){
return Result.error("两次密码不一致");
}
userService.updatePwd(newPwd);
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
operations.getOperations().delete(token);
return Result.success();
}

SpringBoot 项目部署

image-20240622102014405

  1. 引入插件

    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
  2. Maven中Lifecycle执行package命令

  3. 在target目录下,CMD执行命令java -jar big_event-0.0.1-SNAPSHOT.jar

属性配置方式

项目配置文件方式

在项目resources文件夹下application.yml文件下配置

server:
port: 8080

命令行参数方式

java -jar big_event-0.0.1-SNAPSHOT.jar --server.port=8081

环境变量方式

image-20240622104358615

外部配置文件方式

image-20240622104433560

配置优先级

  1. 命令行参数
  2. 操作系统环境变量
  3. Jar 包所在目录下的 application.yml
  4. 项目中 resources 目录下的 application.yml

多环境开发-Profiles

1. 单文件配置

在实际项目的开发过程中,我们程序往往需要在不同环境中运行。例如:开发环境、测试环境和生产环境。

每个环境中的配置参数可能都会有所不同,例如数据库连接信息、文件服务器等等。

SpringBoot 提供的 Profiles 可以用来隔离应用程序配置的各个部分,并在特定环境下指定部分配置生效

  • 如何分隔不同环境的配置?

    ---
  • 如何指定哪些配置属于哪个环境?

spring:
config:
activate:
on-profile: 环境名称
  • 如何指定哪个环境的配置生效?

    spring:
    profiles:
    active: 环境名称

    示例:appliction.yml

    # 通用信息,指定生生效的环境
    #多环境下的共性的属性
    spring:
    profiles:
    active: test
    ---
    # 生产环境
    spring:
    config:
    activate:
    on-profile: pro
    server:
    port: 8081
    ---
    # 开发环境
    spring:
    config:
    activate:
    on-profile: dev
    server:
    port: 8082
    ---
    # 测试环境
    spring:
    config:
    activate:
    on-profile: test
    server:8083

    **注:**如果特定环境中的配置和通用信息冲突了,特定环境配置生效

    2. 多文件配置

开发(dev)、测试(test)、生产(prod)分别建立配置文件

  • application.yml 或者application.properties 用于存放所有环境通用的配置,指定激活的环境

  • application-dev.yml或者application-dev.properties 存放开发环境的特殊配置

  • application-test.yml或者application-test.properties 存放测试环境的特殊配置

  • application-prod.yml或者application-prod.properties 存放生产环境的特殊配置

    image-20240623132335188

    多环境开发 -Pofiles- 分组

image-20240623132458051

示例:

spring:
profiles:
active: dev
group:
"dev": devDB,devServer,devSelf
"pro": proDB,proServer,proSelf
"test": testDB,testServer,testself