- 接下来就对接口的开发不做完整的记录,只是说明几点
- 开发接口的时候根据文档看清参数、地址、请求方式。
- 参数如果不是一个实体类的,建议进行封装
- 根据前端返回数据格式确定在serviceImpl最终返回的数据形式。具体是一个实体类还是封装的类对象
- 因为mp无法进行多表联合查询,当涉及的时候,使用xml或者直接在Mapper中写sql语句。
统一缓存处理(优化)
- 内存的访问速度远远大于磁盘的访问速度(1000倍起)
- 继续用AOP开发缓存功能,缓存加在哪个地方哪个地方就是切点
// 切点
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {
long expire() default 1 * 60 * 1000;
//缓存标识 key
String name() default "";
}
- 定义切面,切面定义了切点和通知的关系
//aop 定义一个切面,切面定义了切点和通知的关系
@Aspect
@Component
@Slf4j
public class CacheAspect {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 切点
@Pointcut("@annotation(com.lbj.blog.common.cache.Cache)")
public void pt(){}
// 环绕通知
@Around("pt()")
public Object around(ProceedingJoinPoint pjp){
try {
Signature signature = pjp.getSignature();
//类名
String className = pjp.getTarget().getClass().getSimpleName();
//调用的方法名
String methodName = signature.getName();
Class[] parameterTypes = new Class[pjp.getArgs().length];
Object[] args = pjp.getArgs();
//参数
String params = "";
for(int i=0; i<args.length; i++) {
if(args[i] != null) {
params += JSON.toJSONString(args[i]);
parameterTypes[i] = args[i].getClass();
}else {
parameterTypes[i] = null;
}
}
if (StringUtils.isNotEmpty(params)) {
//加密 以防出现key过长以及字符转义获取不到的情况
params = DigestUtils.md5Hex(params);
}
// 获取方法
Method method = pjp.getSignature().getDeclaringType().getMethod(methodName, parameterTypes);
//获取Cache注解
Cache annotation = method.getAnnotation(Cache.class);
//缓存过期时间
long expire = annotation.expire();
//缓存名称
String name = annotation.name();
//先从redis获取
String redisKey = name + "::" + className+"::"+methodName+"::"+params;
String redisValue = redisTemplate.opsForValue().get(redisKey);
if (StringUtils.isNotEmpty(redisValue)){
log.info("走了缓存~~~,{},{}",className,methodName);
Result result = JSON.parseObject(redisValue, Result.class);
return result;
}
// 访问方法
Object proceed = pjp.proceed();
redisTemplate.opsForValue().set(redisKey,JSON.toJSONString(proceed), Duration.ofMillis(expire));
log.info("存入缓存~~~ {},{}",className,methodName);
return proceed;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return Result.fail(-999,"系统错误");
}
}
controller加上切点
/**
* 首页 最热文章
* @return
*/
@PostMapping("hot")
@Cache(expire = 5 * 60 * 1000,name = "hot_article")
public Result hotArticle(){
int limit = 5;
return articleService.hotArticle(limit);
}
权限部分
- 先说一下数据库表
ms_admin(权限用户表)
id 权限用户的id,主键
username 权限用户的用户名
password 权限用户的密码
ms_permission(权限表名)
id 权限表的id,主键
name 权限名
path 权限路径
description 权限的描述
ms_admin_permission(权限用户对应权限表的关系表)
id 关系表id,主键
admin_id 权限用户的id
permission_id 权限id
权限管理
controller
- 接口中有权限的列表、增删改
@RestController
@RequestMapping("admin")
public class AdminController {
@Autowired
private PermissionService permissionService;
@PostMapping("permission/permissionList")
public Result permissionList(@RequestBody PageParam pageParam){
return permissionService.listPermission(pageParam);
}
@PostMapping("permission/add")
public Result add(@RequestBody Permission permission){
return permissionService.add(permission);
}
@PostMapping("permission/update")
public Result update(@RequestBody Permission permission){
return permissionService.update(permission);
}
@GetMapping("permission/delete/{id}")
public Result delete(@PathVariable("id") Long id){
return permissionService.delete(id);
}
}
service
@Service
public class PermissionService {
@Autowired
private PermissionMapper permissionMapper;
public Result listPermission(PageParam pageParam){
Page<Permission> page = new Page<>(pageParam.getCurrentPage(),pageParam.getPageSize());
LambdaQueryWrapper<Permission> queryWrapper = new LambdaQueryWrapper<>();
if (StringUtils.isNotBlank(pageParam.getQueryString())) {
queryWrapper.eq(Permission::getName,pageParam.getQueryString());
}
Page<Permission> permissionPage = this.permissionMapper.selectPage(page, queryWrapper);
PageResult<Permission> pageResult = new PageResult<>();
pageResult.setList(permissionPage.getRecords());
pageResult.setTotal(permissionPage.getTotal());
return Result.success(pageResult);
}
public Result add(Permission permission) {
this.permissionMapper.insert(permission);
return Result.success(null);
}
public Result update(Permission permission) {
this.permissionMapper.updateById(permission);
return Result.success(null);
}
public Result delete(Long id) {
this.permissionMapper.deleteById(id);
return Result.success(null);
}
}
- 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 权限配置类
- 自定义的权限服务配置要实现UserDetailsService接口,主要是实现UserDetailsService方法,加密策略选择了BCryptPasswordEncoder
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
//加密策略 MD5 不安全 彩虹表 MD5 加盐
String mszlu = new BCryptPasswordEncoder().encode("mszlu");
System.out.println(mszlu);
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //开启登录认证
// .antMatchers("/user/findAll").hasRole("admin") //访问接口需要admin的角色
.antMatchers("/css/**").permitAll()
.antMatchers("/img/**").permitAll()
.antMatchers("/js/**").permitAll()
.antMatchers("/plugins/**").permitAll()
.antMatchers("/admin/**").access("@authService.auth(request,authentication)") //自定义service 来去实现实时的权限认证
.antMatchers("/pages/**").authenticated()
.and().formLogin()
.loginPage("/login.html") //自定义的登录页面
.loginProcessingUrl("/login") //登录处理接口
.usernameParameter("username") //定义登录时的用户名的key 默认为username
.passwordParameter("password") //定义登录时的密码key,默认是password
.defaultSuccessUrl("/pages/main.html")
.failureUrl("/login.html")
.permitAll() //通过 不拦截,更加前面配的路径决定,这是指和登录表单相关的接口 都通过
.and().logout() //退出登录配置
.logoutUrl("/logout") //退出登录接口
.logoutSuccessUrl("/login.html")
.permitAll() //退出登录的接口放行
.and()
.httpBasic()
.and()
.csrf().disable() //csrf关闭 如果自定义登录 需要关闭
.headers().frameOptions().sameOrigin();
}
}
权限认证
- 因为在配置的时候是使用了自定义service
authService
进行权限认证的
@Service
@Slf4j
public class AuthService {
@Autowired
private AdminService adminService;
public boolean auth(HttpServletRequest request, Authentication authentication){
String requestURI = request.getRequestURI();
log.info("request url:{}", requestURI);
//true代表放行 false 代表拦截
Object principal = authentication.getPrincipal();
if (principal == null || "anonymousUser".equals(principal)){
//未登录
return false;
}
UserDetails userDetails = (UserDetails) principal;
String username = userDetails.getUsername();
Admin admin = adminService.findAdminByUserName(username);
if (admin == null){
return false;
}
if (admin.getId() == 1){
//认为是超级管理员
return true;
}
List<Permission> permissions = adminService.findPermissionsByAdminId(admin.getId());
requestURI = StringUtils.split(requestURI,'?')[0];
for (Permission permission : permissions) {
if (requestURI.equals(permission.getPath())){
log.info("权限通过");
return true;
}
}
return false;
}
}
AdminMapper
public interface AdminMapper extends BaseMapper<Admin> {
}
PermissionMapper
public interface PermissionMapper extends BaseMapper<Permission> {
List<Permission> findPermissionsByAdminId(Long adminId);
}
sql
<mapper namespace="com.mszlu.blog.admin.mapper.PermissionMapper">
<select id="findPermissionsByAdminId" parameterType="long" resultType="com.mszlu.blog.admin.pojo.Permission">
select * from ms_permission where id in (select permission_id from ms_admin_permission where admin_id=#{adminId})
</select>
</mapper>
- 该项目是一个单体项目,麻雀虽小五脏俱全,虽然简简单单的就总结完成了,总结还是根据当时文档进行总结的,自己也对着视频敲了三五天吧。说一下自己对这个项目的感觉
总结
- 任何一个项目给了数据库,在开发对应的接口的时候,应该好好看看对应的数据库表,这样才能进行相应的开发。
- 从最开始的依赖包的管理(maven)
- 设计到新的orm框架-mybatis-plus,其实这个简单的看一下就可以了,大致和mybatis差不多,但是使用是有分页插件的,所以要使用mp的时候要把mp插件配置写好。
- 是一个前后端分离项目,那么前后端分离就需要统一状态返回码
- 在开发的时候,永远牢记controller->service->mapper(dao),每个mapper会继承basemapper,但是mp的缺点是无法进行多表联合查询,所以一旦涉及到了多表联合查询,需要自己手写sql语句,这部分就mybatis的
- 如果xml是在mapper包下,要注意可能出现绑定异常的问题,这个时候就需要pom文件绑定xml以及在配置文件中配置xml文件路径。
- 如果xml在resource下,注意要和包名要一致,此时就不需要再配置和pom绑定了。
- 需要统一的异常处理,针对可以预见的异常进行处理
- 看前端返回数据形式,以及每次请求的参数,如果不是一个实体类能够解决的,那么就进行封装。
- JWT的使用,登陆的时候,密码不能以明文的方式传递到数据库表中,使用md5加盐的方式加密,JWT最关键的就是签名部分,这部分一般会有工具类
- 使用到redis,登陆的时候,服务器生成的token保存到redis中,以后可以直接从redis中进行取,速度快一些。
- 考虑事务,如果登陆的时候redis挂掉,就不能在数据库中保存注册的账户密码等信息,所以使用到
@Transactional
注解,这个注解在底层是ThreadLocal
- 拦截器,如果访问的是一些静态资源就不需要进行拦截,如果是需要登陆的,那么就需要进行拦截判断,同时放行之后将用户信息放在自己定义个ThreadLocal中,但是在使用了ThreadLocal之后一定要remove,不然就会发生内存泄露问题。之后需要可以直接获取。
拦截器的配置。(通常也是写好的,但是还是看一下)
- 线程池的使用,如何去配置线程池的时候要写注解
@EnableAsync
(开启多线程),配置线程池的名字@Bean("taskExecutor")
,以及在相应要使用线程的地方标注@Async("taskExecutor")
- 分布式id在前端会造成精度缺失,使用
@JsonSerialize(using = ToStringSerializer.class)
。
- spring的两大特性:IOC和AOP
- 开发日志和缓存,都使用到了AOP
- 上传图片这些文件可以上传至云服务器上,比如:七牛云、阿里云...
- springsecurity的整合,用来进行权限管理,如果自己要自定义的权限登陆要继承
WebSecurityConfigurerAdapter
,自定义权限服务配置要实现UserDetailsService