前言

随着大环境的影响,互联网寒冬降临,程序员的日子越来越难,搞不好哪天就被噶了,多学点东西也没啥坏处,国内市场java如日中天,出门在外不会写两行java代码,都不好意思说自己是程序员,伪装成一个萌新运维,混迹于各大java群,偷师学艺,略有所获,水一篇博客以记之

本博客仅仅代表作者个人看法,以.Net视角来对比,不存在语言好坏之分,不足之处,欢迎拍砖,以免误人子弟,java大佬有兴趣可以带我一jio,探讨学习,懂的都懂

特殊用语 --> 懂的都懂 形容一些心照不宣的事情,可自行百度谷歌....

环境准备

JDK 1.8
IDEA
Maven 需配置阿里的源

工程结构

.net里工程结构大致如下:
|---解决方案
|---项目A
|---项目B
|---项目C java里工程结构
|---项目
|---模块A
|---模块B
|---模块C
  • 我们先创建一个空模板项目 文件->新建项目-->Empty Project 指定项目名称以及项目路径即可
  • 在该项目路径下,创建对应的模块,比较常用的是Spring,Spring Initializr Maven,这个跟.net类似

starter 中间件

starter 是springboot里提出的一个概念,场景启动器,把一些常用的依赖聚合打包,方便使用者直接在项目中使用,简化了开发,在.net里就是中间件了,一个意思

官方解释
Starters are a set of convenient dependency descriptors that you can include in your application.
You get a one-stop shop for all the Spring and related technologies that you need without having to hunt through sample code and copy-paste loads of dependency descriptors.
For example, if you want to get started using Spring and JPA for database access, include the spring-boot-starter-data-jpa dependency in your project.
spring生态是毋庸置疑的,开发常用的中间件,spring都整理好了,可以去官网直接查阅,懂的都懂

创建一个自定义的starter

  • 新建一个模块,选择Maven模块,给模块取个名字 hello-spring-boot-starter, 取名字要遵循starter的规范,望文知意

  • 再创建一个模块,选择Spring Initializr模块,给模块取个名字 hello-spring-boot-starter-autoconfigure,用于给starter编写装配信息,这样spring就能根据约定,自动装配,hello-spring-boot-starter 依赖于 hello-spring-boot-starter-autoconfigure,当然了如果嫌麻烦,直接在 hello-spring-boot-starter 里写装配信息也可以,这个跟.net里类似,懂的都懂

  • java项目起手式,在src/main/java,创建包路径,通常为公司域名,com.xxx.xxx,我这里定义为com.liang.hello

1.在com.liang.hello下,定义三个包

    autoConfig  用于编写装配信息,生成对象,spring将这些对象添加到IOC容器
bean 用于映射配置文件,将application.yaml里的配置映射为实体类(javabean)
service 用于编写中间件的业务代码,需要使用到配置信息的实体类

2.在bean包下,创建HelloProperties 文件,我定义了两个属性,和一个Student对象

    package com.liang.hello.bean;

    import org.springframework.boot.context.properties.ConfigurationProperties;

    /**
* @ConfigurationProperties("hello")是springboot提供读取配置文件的一个注解
* 1)让当前类的属性和配置文件中以 hello开头的配置进行绑定
* 2)以 hello为前缀在配置文件中读取/修改当前类中的属性值
*/
@ConfigurationProperties("hello")
public class HelloProperties { private String prefix;
private String suffix;
private Student student; public String getPrefix() {
return prefix;
} public void setPrefix(String prefix) {
this.prefix = prefix;
} public String getSuffix() {
return suffix;
} public void setSuffix(String suffix) {
this.suffix = suffix;
} public Student getStudent() {
return student;
} public void setStudent(Student student) {
this.student = student;
}
}
    package com.liang.hello.bean;

    public class Student {
private String name;
private String age; public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public String getAge() {
return age;
} public void setAge(String age) {
this.age = age;
}
}

3.在service包下,定义一个接口,跟一个实现类,简单的输出配置文件的信息

    package com.liang.hello.service;
public interface BaseService {
String sayMsg(String msg);
}
    package com.liang.hello.service;

    import com.liang.hello.bean.HelloProperties;
import org.springframework.beans.factory.annotation.Autowired; public class HelloService implements BaseService{ @Autowired
private HelloProperties helloProperties; public String sayMsg(String msg)
{
return helloProperties.getPrefix()+": "+msg+">> "+helloProperties.getSuffix() + helloProperties.getStudent().getName() + helloProperties.getStudent().getAge();
}
}

4.在autoConfig包下,产生一个bean对象,丢给spring ioc,要标记这个类为一个配置类

    package com.liang.hello.autoConfig;

    import com.liang.hello.bean.HelloProperties;
import com.liang.hello.service.BaseService;
import com.liang.hello.service.HelloService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; @Configuration //标识配置类
@EnableConfigurationProperties(HelloProperties.class)//开启属性绑定功能+默认将HelloProperties放在容器中
public class HelloAutoConfiguration { /**
* @Bean注解用于告诉方法,产生一个Bean对象,然后这个Bean对象交给Spring管理。
* 产生这个Bean对象的方法Spring只会调用一次,随后这个Spring将会将这个Bean对象放在自己的IOC容器中;
*
* @ConditionalOnMissingBean(HelloService.class)
* 条件装配:容器中没有HelloService这个类时标注的方法才生效 / 创建一个HelloService类
*/
@Bean
@ConditionalOnMissingBean(HelloService.class)
public BaseService helloService()
{
BaseService helloService = new HelloService();
return helloService;
}
}
  • 前面都非常简单,就是自己生成了一个对象,然后交给spring ioc管理,接下来就是告诉spring 如何寻找到HelloAutoConfiguration

5.在resources/META-INF 下,创建spring.factories 文件,告诉你配置类的位置,srping会扫描包里这个文件,然后执行装配

    # Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.liang.hello.autoConfig.HelloAutoConfiguration

这样一个简单的starter就写好了,使用maven构建一下,并推送到本地仓库,maven类似于nuget,懂的都懂,现在去创建一个测试项目,来测试一下

6.新建模块,选择Spring Initializr模块,给模块取个名字 hello-spring-boot-starter-test,在pom.xml里添加依赖

        <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.liang</groupId>
<artifactId>hello-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

7.java项目起手式,在src/main/java,创建包路径,定义为com.liang.hello

在resources下,定义application.yaml配置文件

    hello:
prefix: 你好!
suffix: 666 and 888
student:
name: 雪佬
age: 18 server:
port: 8080
servlet:
context-path: /

8.在com.liang.hello下,定义controller包,用于webapi的控制器

定义一个HelloController类,编写一个简单的webapi,来测试自定义的starter,DataResponse是我自定义的一个统一返回类

        @Autowired
BaseService helloService; @ResponseBody
@GetMapping("/hello") //处理get请求方式的/hello请求路径
public DataResponse sayHello() //处理方法
{
String s = helloService.sayMsg("test666777888");
return DataResponse.Success(s,"");
}
  • java语法懂的都懂,由于函数没有可选参数,所以需要写很多重载方法
    package com.liang.hello.common;

    import lombok.Builder;
import lombok.ToString; @Builder
@ToString
public class DataResponse {
/**
* 响应码
*/
public String Code;
/**
* 返回的数据
*/
public Object Data; /**
* 消息
*/
public String Message; public DataResponse(String code,Object data,String message){
Code = code;
Data = data;
Message = message;
} public static DataResponse Error() {
return DataResponse.builder().Code("-1").build();
}
public static DataResponse Error(Object data) {
return DataResponse.builder().Code("-1").Data(data).build();
}
public static DataResponse Error(String message) {
return DataResponse.builder().Code("-1").Message(message).build();
}
public static DataResponse Error(Object data,String message) {
return DataResponse.builder().Code("-1").Data(data).Message(message).build();
} public static DataResponse Success() {
return DataResponse.builder().Code("0").build();
}
public static DataResponse Success(Object data) {
return DataResponse.builder().Code("0").Data(data).build();
}
public static DataResponse Success(String message) {
return DataResponse.builder().Code("0").Message(message).build();
}
public static DataResponse Success(Object data,String message) {
return DataResponse.builder().Code("0").Data(data).Message(message).build();
} }
弄到这里starter就结束了嘛,显然事情没有这么简单,既然用到了spring的自动装配,那我们不妨往深处再挖一挖,没准有意外收获哦

前面我们已经创建了HelloService,那再创建一个TestService,同样继承BaseService,然后HelloAutoConfiguration类下,在写一个testService的bean,测试一下一个接口多个实现,如何获取指定的实例

非常神奇,spring会自动匹配,根据变量名称,自动匹配bean,点击左侧spring的绿色小图标(类似于断点图标),还能自动跳转到bean的实现,不要问,问就是牛逼,懂的都懂

现在我们已经在starter里创建了2个bean,如果有N个bean,每个bean都要去HelloAutoConfiguration类下写装配,真是太麻烦了,这个时候,就可以使用到spring的自动装配注解,只用在testService类上,加一个@Service的注解,就搞定了,简单方便,连spring.factories都不用写,在.net里的DI框架目前还没有统一,有内置的,用的比较多的是autofac,还有自研的DI框架,都大同小异

项目结构

springboot

springboot现在已经是java web开发的主流了,通常我们用.net core来之对标,他们诞生的初衷完全不一样,springboot是整合自身的生态,化繁为简,starter就是非常具有代表性的特性之一,.net core是一套跨平台方案,诞生之初就是为了跨平台,本身就非常简洁,易用性也非常高,开发者往里面添加所需的中间件即可,它的关注点始终围绕框架的简洁与性能

选择springboot脚手架项目,会自动创建一个启动文件HelloSpringBootStarterTestApplication 里面有一个@SpringBootApplication的组合注解,想了解的可以去翻阅java八股文,这里我加了一个@EntityScan("com.liang.hello")注解,用于自动扫描该包下的bean,并完成装配

控制器类上,要加@RestController 注解,这也是一个组合注解,然后在方法上加@ResponseBody注解,用于返回json类型,指定方法映射的路由,就可以了,如果想做mvc项目,还需要下载模板引擎的依赖,修改返回类型,指向一个视图,略微麻烦些

aop

  • 创建完springboot webapi模块,我们需要添加一个切面,用于记录请求的信息

    java里分为过滤器与拦截器,过滤器依赖与servlet容器,拦截器是Spring容器的功能,本质上都是aop思想的实现

    .net core里内置了各种过滤器,方便我们直接使用,拦截器则使用的比较少

1.老步骤,添加maven依赖

        <dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.9.1</version>
</dependency>
  • 定义一个SpringBootAspect的类,用于AOP拦截,先定义一个切入点,再定义切面处理逻辑,这里主要定义一个控制器全局异常处理
        @Aspect
@Component
public class SpringBootAspect { /**
* 定义一个切入点
*/
@Pointcut(value="execution(* com.liang.hello.controller.*.*(..))")
public void aop(){} @Around("aop()")
public Object around(ProceedingJoinPoint invocation) throws Throwable{
Object res = null;
System.out.println("SpringBootAspect..环绕通知 Before");
try { res = invocation.proceed();
}catch (Throwable throwable){
//修改内容
System.out.println("页面执行错误,懂的都懂");
res = new DataResponse("500",null,"页面执行错误");
}
System.out.println("SpringBootAspect..环绕通知 After");
return res;
} }

ide提示异常,java规定,结束语句后面,不允许有代码,他们认为编译器不执行的代码是垃圾代码,呔,java语法懂的都懂,略施小计,成功的骗过了ide

  • 执行结果

定时任务

  • 定时任务是工作中使用非常频繁的部分,也有很多框架,但是一些简单的内置任务,使用框架就有点杀鸡用牛刀了,.net里我们通常用HostedService来实现,springboot内置了定时任务

1.创建一个ScheduledTasks类,使用注解开启异步,HelloSpringBootStarterTestApplication类也要开启哦,代码如下

    @EnableAsync
@Component
public class ScheduledTasks {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss"); /**
* 任务调度,每隔1秒执行一次
*/
@Async
@Scheduled(fixedRate = 1000)
public void reportCurrentTime() { runThreadTest(1); } public void runThreadTest(int i) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程"+Thread.currentThread().getName()+"执行异步任务"+i + "现在时间:" + dateFormat.format(new Date())); }
}

  • runThreadTest方法,堵塞3秒,模拟业务执行耗时,发现定开启了异步,但是它依旧是同步执行,需要等上一个任务执行完毕,才会再执行下一个任务,网上翻了下答案,需要配置线程池,代码如下
    package com.liang.hello.config;

    import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; @Configuration
public class ExecutorConfig implements AsyncConfigurer { // ThredPoolTaskExcutor的处理流程
// 当池子大小小于corePoolSize,就新建线程,并处理请求
// 当池子大小等于corePoolSize,把请求放入workQueue中,池子里的空闲线程就去workQueue中取任务并处理
// 当workQueue放不下任务时,就新建线程入池,并处理请求,
// 如果池子大小撑到了maximumPoolSize,就用RejectedExecutionHandler来做拒绝处理
// 当池子的线程数大于corePoolSize时,多余的线程会等待keepAliveTime长时间,如果无请求可处理就自行销毁
//getAsyncExecutor:自定义线程池,若不重写会使用默认的线程池。
@Override
@Bean
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
//设置核心线程数
threadPool.setCorePoolSize(10);
//设置最大线程数
threadPool.setMaxPoolSize(20);
//线程池所使用的缓冲队列
threadPool.setQueueCapacity(10);
//等待任务在关机时完成--表明等待所有线程执行完
threadPool.setWaitForTasksToCompleteOnShutdown(true);
// 等待时间 (默认为0,此时立即停止),并没等待xx秒后强制停止
threadPool.setAwaitTerminationSeconds(60);
// 线程名称前缀
threadPool.setThreadNamePrefix("ThreadPoolTaskExecutor-"); // 初始化线程
threadPool.initialize();
return threadPool;
}
}

执行结果

mybatis plus

  • 目前java主流的ORM框架,应该是mybatis了,我是不怎么喜欢在xml里组织sql的,麻烦的一批,但是也避免了萌新为图方便,sql写的到处都是,维护起来懂的都懂,网上随便翻个答案,直接往项目里整合

1.老样子先添加依赖

        <!--mybatis-plus的springboot支持-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.1</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-core</artifactId>
<version>3.4.3.1</version>
</dependency>

2.然后在yaml文件里添加mysql与mybatis plus的配置

    spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://************:3306/test_mybatis?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: ******
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
serialization:
write-dates-as-timestamps: false mybatis-plus:
configuration:
map-underscore-to-camel-case: false
auto-mapping-behavior: full
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:mapper/*.xml
global-config:
# 逻辑删除配置
db-config:
# 删除前
logic-not-delete-value: 1
# 删除后
logic-delete-value: 0

3.再整一个mybatis plus的配置类,添加mybatis plus的拦截器,反正也是网上抄的,我猜测大致是这个意思

    package com.liang.hello.config;

    import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; @Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

现在开始添加创建mybatis的相关目录,网上都有,直接跟着抄就可以了,也非常好理解

4.定义一个mapper的包,在包下编写mybatis的接口,我这里用的是mybatis plus,已经默认实现了CRUD,我们简单的写几个接口,用来测试

    package com.liang.hello.mapper;

    import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.liang.hello.dto.OrderInfoResponse;
import com.liang.hello.entity.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
import java.util.List; @Repository
@Mapper
//表明这是一个Mapper,也可以在启动类上加上包扫描
//Mapper 继承该接口后,无需编写 mapper.xml 文件,即可获得CRUD功能
public interface UserInfoMapper extends BaseMapper<UserInfo> { @Select("select u.*,o.id as orderId,o.price from user_info u left join order_info o on u.id = o.userId ${ew.customSqlSegment}")
List<OrderInfoResponse> getAll(@Param(Constants.WRAPPER) Wrapper wrapper); List<UserInfo> selectByName(@Param("UserName") String userName); void updateUserInfo(@Param("UserName") String userName,@Param("Age") int age);
}
  • 简单的sql,mybatis plus也支持直接使用注解的方式来执行,简单方便,参数是通过queryWrapper条件构造器来完成的,喜欢的同学可以重点了解一下,.net里有linq,用过的同学懂的都懂

    另外一个方式,就是通过制定mapper.xml来编写sql,xml文件路径在配置文件里制定,我们按照约定即可,在resources/mapper下,创建UserInfo.xml,名称空间指向接口路径,id对应接口的名称,返回类型指向对应的实体

    <?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.com.liang.hello.mapper.UserInfoMapper"> <select id="selectByName" resultType="com.liang.hello.entity.UserInfo">
select * from user_info
<where>
<if test="UserName != null and UserName != ''">
UserName like CONCAT('%',#{UserName},'%');
</if>
</where>
</select> <select id="updateUserInfo" resultType="com.liang.hello.entity.UserInfo">
update user_info set Age=#{Age}
<where>
<if test="UserName != null and UserName != ''">
UserName = #{UserName};
</if>
</where>
</select>
</mapper>

mybatis xml语法可以去学习下,也不困难,简单的看一遍就差不多了,复杂的部分用到的时候再去翻阅

5.定义一个service的包,在包下创建UserInfoServiceImpl,很熟悉的味道,经典的三层架构,在service层编写业务逻辑,调用mapper接口的增删改查方法,这里重点说下事务

spring提供了事务的注解@Transactional,使用起来也非常方便,原理应该是借助AOP来实现,使用这个注解前需要事先了解事务失效的场景,老八股文了,懂的都懂,在.net里使用手动提交事务比较多,特意去了解搜了下手动提交事务,感觉差不多

    //修改年龄
@Transactional
public void update(UserInfo entity){
// TransactionStatus txStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());
//
// try {
// userInfoMapper.updateUserInfo(entity.getUserName(),entity.getAge());
// if(true) {
// throw new Exception("xx");
// }
// userInfoMapper.updateUserInfo(entity.getUserName(),entity.getAge()+1);
//
// } catch (Exception e) {
// transactionManager.rollback(txStatus);
// e.printStackTrace();
// }finally {
// transactionManager.commit(txStatus);
// } //执行第一条sql
userInfoMapper.updateUserInfo(entity.getUserName(),entity.getAge());
if(true) throw new RuntimeException();
//执行第二条sql
userInfoMapper.updateUserInfo(entity.getUserName(),entity.getAge()+1);
}

jwt

  • 现在前后端分离已经成为主流,jwt是首选方案,话不多说,直接往里面怼

1.老规矩,先添加jwt的依赖

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

2.先定义一个工具类JwtUtils,用于jwt的一些常规操作,当看到verifyToken这个方法的时候,我就发现事情没有那么简单

    package com.liang.hello.common;

    import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map; public class JwtUtils {
// 过期时间 24 小时 60 * 24 * 60 * 1000
private static final long EXPIRE_TIME = 60 * 60 * 1000;//60分钟
// 密钥
private static final String SECRET = "uxzc5ADbRigUDaY6pZFfWus2jZWLPH1";
private static String json=""; /**
* 生成 token
*/
public static String createToken(String userId) {
try {
// 设置过期时间
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
// 私钥和加密算法
Algorithm algorithm = Algorithm.HMAC256(SECRET);
// 设置头部信息
Map<String, Object> header = new HashMap<>(2);
header.put("Type", "Jwt");
header.put("alg", "HS256"); // 返回token字符串 附带userId信息
return JWT.create()
.withHeader(header)
.withClaim("userId", userId)
//到期时间
.withExpiresAt(date)
//创建一个新的JWT,并使用给定的算法进行标记
.sign(algorithm); } catch (Exception e) {
return null;
}
} /**
* 校验 token 是否正确
*/
public static Map<String, Claim> verifyToken(String token){
token = token.replace("Bearer ","");
DecodedJWT jwt = null;
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
jwt = verifier.verify(token); } catch (TokenExpiredException e) {
//效验失败
//这里抛出的异常是我自定义的一个异常,你也可以写成别的
throw new TokenExpiredException("token校验失败");
}
return jwt.getClaims();
} /**
* 获得token中的信息
*/
public static String getUserId(String token) {
Map<String, Claim> claims = verifyToken(token);
Claim user_id_claim = claims.get("userId");
if (null == user_id_claim || StringUtils.isEmpty(user_id_claim.asString())) {
return null;
}
return user_id_claim.asString();
} }
  • 校验token正确,从token中获取信息,在.net里框架帮忙做了,使用起来非常简单,emmmmm.....我觉得spring提供一个spring-boot-starter-jwt 很有必要

.net里实现如下

            //认证参数
services.AddAuthentication("Bearer")
.AddJwtBearer(o =>
{
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,//是否验证签名,不验证的话可以篡改数据,不安全
IssuerSigningKey = signingKey,//解密的密钥
ValidateIssuer = true,//是否验证发行人,就是验证载荷中的Iss是否对应ValidIssuer参数
ValidIssuer = jwtOptions.Iss,//发行人
ValidateAudience = true,//是否验证订阅人,就是验证载荷中的Aud是否对应ValidAudience参数
ValidAudience = jwtOptions.Aud,//订阅人
ValidateLifetime = true,//是否验证过期时间,过期了就拒绝访问
ClockSkew = TimeSpan.Zero,//这个是缓冲过期时间,也就是说,即使我们配置了过期时间,这里也要考虑进去,过期时间+缓冲,默认好像是7分钟,你可以直接设置为0
RequireExpirationTime = true,
};
o.Events = new JwtBearerEvents
{
//权限验证失败后执行
OnChallenge = context =>
{
//终止默认的返回结果(必须有)
context.HandleResponse();
var result = JsonConvert.SerializeObject(new { code = "401", message = "验证失败" });
context.Response.ContentType = "application/json";
//验证失败返回401
context.Response.StatusCode = StatusCodes.Status200OK;
context.Response.WriteAsync(result);
return Task.FromResult(0);
}
};
});
  • 思路应该比较简单,弄个拦截器,校验一波jwt,完成认证,再通过jwt里的userId校验用户是否拥有访问权限,开干

3.先整一个自定义的注解AllowAnonymousAttribute,允许匿名访问,标识这个注解可以作用于类和方法上

    package com.liang.hello.attribute;

    import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; @Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AllowAnonymousAttribute {
boolean required() default true;
}
  • 编写自定义拦截器,用于jwt的校验,校验通过,获取用户信息并授权,这里主要是获取类跟方法有没有使用自定义注解,HandlerInterceptorAdapter也提示已过期,不知道有没有替代方案
    package com.liang.hello.filters;

    import com.auth0.jwt.interfaces.Claim;
import com.liang.hello.attribute.AllowAnonymousAttribute;
import com.liang.hello.common.JwtUtils;
import com.liang.hello.entity.UserInfo;
import com.liang.hello.service.UserInfoServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.security.SignatureException;
import java.util.Map; @Component
public class JwtFilter extends HandlerInterceptorAdapter {
@Autowired
UserInfoServiceImpl userInfoService; @Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws SignatureException {
// 如果不是映射到方法直接通过
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
AllowAnonymousAttribute actionAttribute= handlerMethod.getMethod().getDeclaredAnnotation(AllowAnonymousAttribute.class);
AllowAnonymousAttribute controllerAttribute = handlerMethod.getBeanType().getDeclaredAnnotation(AllowAnonymousAttribute.class); if (actionAttribute!=null || controllerAttribute!=null) return true;
//默认全部检查
System.out.println("被jwt拦截需要验证");
// 从请求头中取出 token 这里需要和前端约定好把jwt放到请求头一个叫Authorization的地方,**<font color=red size=3>懂的都懂</font>**
String token = httpServletRequest.getHeader("Authorization");
// 执行认证
if (token == null) {
//这里其实是登录失效,没token了 这个错误也是我自定义的,读者需要自己修改
throw new SignatureException("自定义错误");
}
// 获取 token 中的 user Id
String userId = JwtUtils.getUserId(token); //找找看是否有这个user 因为我们需要检查用户是否存在,读者可以自行修改逻辑
UserInfo user = userInfoService.getUserInfoById(userId); if (user == null) {
//这个错误也是我自定义的
throw new SignatureException("自定义错误");
}
//放入attribute以便后面调用
httpServletRequest.setAttribute("userName", user.getUserName());
httpServletRequest.setAttribute("id", user.getId());
httpServletRequest.setAttribute("age", user.getAge());
return true;
} }

4.注册自定义拦截器,让spring调用这个拦截器

    package com.liang.hello.config;

    import com.liang.hello.filters.JwtFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import javax.annotation.Resource; @Configuration
public class WebConfig implements WebMvcConfigurer {
@Resource
private JwtFilter jwtFilter ;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtFilter).addPathPatterns("/**");
}
}

异常处理

前面我们有使用过框架自定义的一些异常,TokenExpiredException,SignatureException,我们可以在SpringBootAspect里处理这些异常,并给出友好提示

    @ExceptionHandler(value = {TokenExpiredException.class})
public DataResponse tokenExpiredException(TokenExpiredException e){
return new DataResponse("401",null,"权限不足token失效");
} @ExceptionHandler(value = {SignatureException.class})
public DataResponse authorizationException(SignatureException e){
return new DataResponse("401",null,"权限不足");
}
//全局异常,兜底方案
@ExceptionHandler(value = {Exception.class})
public DataResponse exception(Exception e){
return new DataResponse("500",null,"系统错误");
}
  • 未登录访问需要授权接口

  • 登录,使用错误的用户名

  • 登录,使用正确的用户名

  • 使用token,访问需要授权接口

    主动抛出异常



    正常执行

  • token过期,访问需要授权接口

  • 使用错误token,访问需要授权接口,因为没有主动捕获该异常,被全局异常统一处理

最新文章

  1. Linux学习之路&mdash;Linux文件与目录管理
  2. Retrofit源码分析(一)
  3. MSSQL数据库链接字符串Asynchronous Processing=true不是异步查询吗,怎么是缓存
  4. oracle imp导入库到指定表空间
  5. SQL将金额转换为汉子
  6. (08)odoo继承机制
  7. C# 笛卡尔积
  8. 打造万能的ListView GridView 适配器
  9. Android. Scrolling 2 listviews together
  10. 对bigDecimal的一些探索
  11. 初学者自学笔记-this的用法
  12. 各种数据库的批量插入操作_Oracle
  13. oracle 计算两个时间之间的月份差,相差几个星期,相差多少天
  14. Java 网络编程(一) 网络基础知识
  15. 使用openXML 不用插件导出excel
  16. 理解maven的核心概念
  17. CSS 简介、 选择器、组合选择器
  18. JAVA核心技术I---JAVA基础知识(Jar文件导入导出)
  19. IDEA快捷键之for循环
  20. JDBC告警系列(一)The server time zone value &#39;&#214;&#208;&#39; is unrecognized or represents more than one time zone.

热门文章

  1. 类和实例,super()函数
  2. Fluentd部署:错误排查
  3. 解析库beautifulsoup
  4. [题解] Atcoder ARC 142 D Deterministic Placing 结论,DP
  5. spring cron表达式源码分析
  6. C#-9 委托
  7. day05多表查询01
  8. H3C交换机配置DHCP服务器
  9. C语言常见的八大排序(详解)
  10. 还在使用@Autowrired注入?不妨试试@RequiredArgsConstructor